Search code examples
angulartypescriptangular-reactive-forms

Angular 18 Reactive form ERROR : There is no FormControl instance attached to form control element with name: 'q1124'


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


Solution

  • 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();
        }
      }
    

    stackblitz