简体   繁体   中英

Setting Values From API for Angular 2 Dynamic Form

I am trying to get started with creating a dynamic form in Angular 2, and I am using the setup from the Angular cookbook here as my starting point. I didn't have any issues with their setup, which just hard codes the data in the service as apposed to an api call. My issue is that when I try to use an api call the values don't seem to get set correctly.

In the Angular cookbook they have the question.service.ts file hard coded as:

getQuestions() {
let questions: QuestionBase<any>[] = [
  new DropdownQuestion({
    key: 'brave',
    label: 'Bravery Rating',
    options: [
      {key: 'solid',  value: 'Solid'},
      {key: 'great',  value: 'Great'},
      {key: 'good',   value: 'Good'},
      {key: 'unproven', value: 'Unproven'}
    ],
    order: 3
  }),
  new TextboxQuestion({
    key: 'firstName',
    label: 'First name',
    value: 'Bombasto',
    required: true,
    order: 1
  }),
  new TextboxQuestion({
    key: 'emailAddress',
    label: 'Email',
    type: 'email',
    order: 2
  })
 ];
   return questions.sort((a, b) => a.order - b.order);
 }
}

And then from the app.component.ts file its simply called from the constructor as:

 constructor(service: QuestionService) {
 this.questions = service.getQuestions();
 }

Which "questions" then binds to this in the app.component.ts template

 <dynamic-form [questions]="questions"></dynamic-form> 

I made changes to the question.service.ts to make an api call (now currently from a json file cause I don't have access to the api at home)

 getFirstQuestion() {
    return this._http.get(this.__questionJSONApiBaseUrl)
        .map(data => data.json())
        .do(data => console.log('All: ' + JSON.stringify(data)))
        .catch(this.handleError);
}

Which is called from the app.component.ts as

ngOnInit() {
    this.service.getFirstQuestion()
        .subscribe(data => {
            this.data = data;

            if (data.Type == 'TextBox') {
                let questions: QuestionBase<any>[] = [
                    new TextboxQuestion({
                        key: data.Title,
                        label: data.Text,
                        value: '',
                        required: true,                          
                        order: data.Id
                    })];
            }              
        }
        );
 }

As you see I set the properties inside of the .subscribe(), but it doesn't seem to be working correctly because when it binds to [questions] in the template I get a "Cannot read property 'forEach' of undefined" error which comes from the question-control.service file which transforms the question to a FormGroup.

I know that the data is coming in because I can set an alert inside the if statement and see the data from the api call successfully. I believe my issue is that [questions] is binding before the data is ready. Can someone tell me a better way to do this or please provide any suggestions to what I'm doing wrong please? Is there a way I could set the properties in the api first?

The issue here, as @silentsod pointed out, is that you're doing an async operation and trying to store that as your questions. You need to handle the async.

You can go about this two ways...either, from the component class do:

service.getQuestions((questions) => this.questions = questions);

Or, you could, from the template, use the async pipe:

<dynamic-form [form]="questions | async"></dynamic-form>

The async pipe subscribes to the observable, in this case questions , and returns the value.

I was able to get things working using ideas from this example with modifications to suit my needs.

The way I have it setup now is that the page loads and a user will have to click a start button to get their first question. So I am no longer calling for my first question inside of ngOnInt, but inside of my buttons click event method like so:

 getFirstQuestion() {
    //hide the begin survey button once clicked
    this.clicked = true;
    this.service.getQuestion()
        .subscribe(
        q => {
            this.questions = q
        });
}

Which getQuestion() in my service looks like this:

  getQuestion() {
    return this.http.get(this._questionApiBaseUrl + 'getFirstQuestion' + '?'              'questionId=' + this.questionId)
        .map(res => [res.json()])
        .map(data => {
            this.data = data;            
            return this.buildQuestions(data);
        })            
}

Which returns this buildQuestions() method:

 buildQuestions(data: any[]) {
    let questions: any[] = [];
    data.forEach((item: QuestionsGroup) => {
        console.log("my item control in buildQuestion: " + item.controlType);
        if (item.controlType == 'group') {
            let group = new QUESTION_MODELS[item.controlType](item);
            group.questions = this.buildQuestions(item.questions);
            questions.push(group);
        }
        else if (item.controlType == 'RadioButtons') {
            item.controlType = 'radio';
            questions.push(new QUESTION_MODELS[item.controlType](item));
        }
        else if (item.controlType == 'TextBox'){
            item.controlType = 'textbox'; 
            item.type = 'text'               
            questions.push(new QUESTION_MODELS[item.controlType](item));
        }
        else if (item.controlType == 'Datepicker') {
            item.controlType = 'textbox';
            item.type = 'date'
            questions.push(new QUESTION_MODELS[item.controlType](item));
        }
        //TODO add any remaining question types
        else {
            questions.push(new QUESTION_MODELS[item.controlType](item));
        }
    });
    return questions;
}

The buildQuestions() method above will get refactored later as there is currently a mismatch of property values coming from the api to the client side.

The way I was trying to do things before was to manipulate the data once it was returned from my call, but the binding to "questons" had already happened. Now when "questions" is returned from the http get call its in the correct format already when I subscribe to it. From here a user will submit an answer and get another question dynamically based on their answer.

Below is my service method to post the answer and get the next question (had to leave a few details out but you get the idea):

 submitAnswer(answer: any) {
    //Interface to formulate my answer 
    var questionAnswer: IAnswer = <any>{};     

   //set answer properties interface here

    let headers = new Headers({ 'Content-Type': 'application/json; charset=utf-8' });
    let options = new RequestOptions({ headers: headers });
    let myAnswerObj = JSON.stringify(questionAnswer);

    return this.http.post(this._questionApiBaseUrl + 'submitanswer', myAnswerObj, options)
        .map(res => [res.json()])
        .map(data => {
            this.data = data;
            //return the next question to the user
            return this.buildQuestions(data);
        });          
}

The ngOnInit() function of dynamic-form.component.ts runs before we receive response from api.

below is the file

dynamic-form.component.ts

 import { Component, Input, OnInit } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { QuestionBase } from './question-base'; import { QuestionControlService } from './question-control.service'; @Component({ selector: 'dynamic-form', templateUrl: './dynamic-form.component.html', providers: [ QuestionControlService ] }) export class DynamicFormComponent implements OnInit { @Input() questions: QuestionBase<any>[] = []; form: FormGroup; payLoad = ''; constructor(private qcs: QuestionControlService) { } ngOnInit() { this.form = this.qcs.toFormGroup(this.questions); } onSubmit() { this.payLoad = JSON.stringify(this.form.value); } } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM