Angular 6 Dynamic Forms issue

2019-08-31 17:26发布

问题:

I'm trying to master dynamic forms following the official documentation but with my own case.

I have api that I'm trying to fetch data from https://demo7782009.mockable.io/form and print it out in corresponding form fields. Each field has own validation requirements.

But getting an error

Error: formGroup expects a FormGroup instance. Please pass one in.

   Example:


<div [formGroup]="myGroup">
  <input formControlName="firstName">
</div>

In your class:

this.myGroup = new FormGroup({
   firstName: new FormControl()
});

I've tried to make a plunker with my app, but it doesn't bootstrap

So there is my code (sorry for that)

App.component.ts

import { FetchFormService } from './fetch-form.service';
import { QuestionControlService } from './question-control.service';
import { Component, OnInit } from '@angular/core';
import { QuestionBase } from './Models/question-base';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  questions: QuestionBase<any>[];
  form: FormGroup;

  constructor(
    private qcs: QuestionControlService,
    private ffs: FetchFormService
  ) {
  }

  ngOnInit() {
    this.ffs.getData().subscribe(data => {
      this.questions = data.data;
      let formQuestions: [QuestionBase<any>];
      this.questions.forEach(e => {
        formQuestions.push(new QuestionBase(e));
      });
      this.form = this.qcs.toFormGroup(formQuestions);
    });
  }

}

App.component.html

<form [formGroup]="form">
  <div *ngFor="let question of questions; let i = index">
    <app-question-item [question]="question" [index]='i' [form]="form"></app-question-item>
  </div>

  <button [disabled]="!form.valid">Submit</button>
</form>

QuestionBase model

export class QuestionBase<T> {
  value: T;
  values?: string[];
  label: string;
  type: string;

  constructor(options: {
    value: T,
    values?: string[],
    label: string,
    type: string
  }) {
    this.value = options.value;
    this.label = options.label;
    this.type = options.type;
    this.values = options.values;
  }
}

Question-item.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { QuestionBase } from '../Models/question-base';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'app-question-item',
  templateUrl: './question-item.component.html',
  styleUrls: ['./question-item.component.css']
})
export class QuestionItemComponent implements OnInit {

  @Input() question: QuestionBase<any>;
  @Input() form: FormGroup;
  @Input() index;

  constructor() { }

  ngOnInit() {
  }

}

Question-item.component.html

<div [formGroup]="form">
  <label>
    {{ question.label }}

    <ng-container [ngSwitch]="question.type">
      <input [formControlName]="'field' + index" *ngSwitchCase="'string' || 'integer' || 'double'" type="text">

      <textarea [formControlName]="'field' + index" *ngSwitchCase="'text'" cols="30" rows="10"></textarea>

      <ul *ngSwitchCase="'list'">
        <li *ngFor="let item of question.values; let subindex = index">
          <input [formControlName]="'field' + index + subindex" type="text">
        </li>
      </ul>
    </ng-container>

  </label>
</div>

Fetch-form.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

interface Form {
  data: {
    type: string,
    label: string,
    value: string,
    values?: string[]
  }[];
}

@Injectable({
  providedIn: 'root'
})
export class FetchFormService {

  constructor(
    private http: HttpClient
  ) { }

  getData() {
    return this.http.get<Form>('https://demo7782009.mockable.io/form');
  }
}

Question-control.service.ts

import { QuestionBase } from './Models/question-base';
import { Injectable } from '@angular/core';
import { FormControl, Validators, FormGroup } from '@angular/forms';

@Injectable({
  providedIn: 'root'
})
export class QuestionControlService {
  constructor() { }

  toFormGroup(questsions: QuestionBase<any>[]) {
    let group: any = {};

    questsions.forEach((e, i) => {
      if (e.type === 'string' || e.type === 'text' || e.type === 'list') {
        group[i] = new FormControl(e.value, Validators.required);
      } else if (e.type === 'integer') {
        group[i] = new FormControl(e.value, [Validators.required, Validators.pattern(/\d+/)]);
      } else if (e.type === 'double') {
        group[i] = new FormControl(e.value, [Validators.required, Validators.pattern(/\d+\.\d+/)]);
      } else {}
    });

    return new FormGroup(group);
  }
}

App.module.ts

import { QuestionItemComponent } from './question-item/question-item.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
    QuestionItemComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Continuation of this questions is here

Angular 6 Dynamic Forms issue [part 2]

回答1:

You declared you form

form: FormGroup;

But forgot to instanciate it. You did this

this.form = this.qcs.toFormGroup(...);

But you are waiting for your HTTP call to complete, meaning that it's undefined until the call is done.

Consider conditioning your HTML :

<form [formGroup]="form" *ngIf="form">

By the way, for sandboxes, consider using http://stackblitz.io



回答2:

Your mistake is quite common, it has nothing to do with dynamic forms. When you init your component, the html template is loaded. Before that your ngOnInit is fired but it is still waiting for the response from your backend. Therefor your "form" variable is undefined. Because your template is being loaded with [formGroup]="undefined" you get that error.

<form [formGroup]="form" *ngIf="form">

The ngIf fixes the problem because it will make sure your form template isn't trying to load before you are sure your formgroup is created.