I have an angular component that will create and render a form dynamically using ReactiveFormsModule. When I press submit it will post the data and I will get new questions for the component to render and replace the old ones.
The first render is fine when you navigate to the page. If I submit the first question the parent will get new questions to pass down to the this component
this error : "There is no FormControl instance attached to form control element with name: 'q1124'
"
Whenever I log the form.controls it does say it contains the 'q1124'
form.component.ts
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { Question } from '../../../../shared/models/views-models/question-view';
import { getQuestionAnswers } from '../../../../shared/utils/helpers.util';
import { MatButtonModule } from '@angular/material/button';
import { MatCard } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@Component({
selector: 'app-survey-form',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatCard, MatButtonModule,],
templateUrl: './survey-form.component.html',
styleUrl: './survey-form.component.scss'
})
export class SurveyFormComponent implements OnChanges {
form: FormGroup = new FormGroup({});
@Input() questions: Question[] = []
@Input() currentState: string | undefined;
@Input() previousState: string | undefined;
@Output() dataEmitter: EventEmitter<any[]> = new EventEmitter<any[]>();
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }
ngOnChanges(changes: SimpleChanges): void {
if (changes['questions'] && changes['questions'].currentValue !== changes['questions'].previousValue) {
this.form = this.createForm(this.fb, this.questions)
this.cdr.detectChanges();
}
}
submit() {
if (this.form.valid) {
const answers = getQuestionAnswers(this.form, this.questions)
this.dataEmitter.emit(answers)
}
}
createForm = (
formBuilder: FormBuilder,
questions: any[]
): FormGroup => {
const group: { [key: string]: AbstractControl<any, any> } = {};
questions.forEach(question => {
group[question.id] = question.mandatory
? formBuilder.control('', [Validators.required])
: formBuilder.control('');
});
return new FormGroup(group)
}
}
form.component.html
<div class="form-container">
<form [formGroup]="form" (ngSubmit)="submit()" class="input-form">
@for (question of questions; track $index) {
<input type="text" matInput [formControlName]="question.id" [required]="question.mandatory"
placeholder="Type your answer here..." [id]="question.id">
}
@if (currentState) {
<button type="submit" [disabled]="form.invalid" mat-flat-button>Next</button>
}
</form>
</div>
I've tried different hooks like ngOnAfterInit to see if this could be some timing related, I also added the changeDetectorRef
UPDATE Link to Stackblitz with mockup https://stackblitz.com/edit/stackblitz-starters-4ewzry?file=src%2Fapp%2Fform-test%2Fform-test.component.ts
the problem is that questions have values before the form is created
You can iterate over form.controls|keyvalue
@for (control of form.controls|keyvalue; track $index) {
<!--see that you use questions[$index].id instead of question-->
<input type="text" matInput [formControlName]="questions[$index].id">
}
But see that you need wait to get the questions
ngOnChanges(changes: SimpleChanges): void {
if (
changes['questions'] &&
changes['questions'].currentValue !== changes['questions'].previousValue
) {
//enclosed in a setTimeout without "delay"
setTimeout(()=>{
this.form = this.createForm(this.fb, this.questions);
})
this.cdr.detectChanges();
}
}