angularasynchronousangular-dynamic-forms

Angular - adding dynamically formControls only loads partial data


I'm working on an Angular form that gets some data from a service in order to populate a form with inputs. Let's say this is a form to order books. The user previously selected a genre, and the service returned a list of books that fit within that genre. Because we will not always get the same number of books depending on the genre, I had to find a turnaround in order to generate inputs with different formControlName. Here is what my form looks like (a simplified version so hopefully I didn't make any typo):

myForm.html

    <form name="form" [formGroup]="mainForm">
        <ng-template ngFor let-book [ngForOf]='datas.books'>
            <div>
                <span> {{book.title}}</span>
            </div>
            <div *ngIf="myBookFG.contains('input'+book.isbn)">
                <mat-form-field>
                    <mat-label>Amount to order</mat-label>
                    <input matInput maxlength='16'formControlName='input{{book.isbn}}' >
                </mat-form-field>
            </div>
        </ng-template>
    </form>

myForm.ts

mainForm!: FormGroup;
myBookFG!: FormGroup;
    



whenSelectedGenreIsChanged(): void {
    //[...]
    this.bookService.getBooksFromGenre(this.selectedGenre)
    .subscribe( (next) => {
        if (next){
            this.datas = next;
            this.changeBookList();
        }
    });
}

changeBookList(): void{
    // Resetting the previous formGroup
    this.mainForm.removeControl('myBookFG');
    // Then creating a new one populated with the books of the selected genre
    this.myBookFG= this.formBuilder.group([]);
    for (const book of datas.books){
        const inputName = "input" + book.isbn;
        this.myBookFG.addControl(inputName,  this.formBuilder.control( '', []));
    }
        
    this.mainForm.addControl('myBookFG', this.myBookFG);
    console.log("Books added : ", this.myBookFG); //Checking if all books controls are imported
}

Now the thing is: this compiles just fine, however, when loading the page, I will only get at best a couple of inputs displayed on the list, and this error from the browser console:

ERROR Error: Cannot find control with name: 'inputABC123'
    at _throwError (forms.mjs:1778:11)
    at setUpControl (forms.mjs:1567:13)
    at FormGroupDirective.addControl (forms.mjs:5337:9)
    at FormControlName._setUpControl (forms.mjs:5893:43)
    at FormControlName.ngOnChanges (forms.mjs:5838:18)
    at FormControlName.rememberChangeHistoryAndInvokeOnChangesHook (core.mjs:1515:14)
    at callHook (core.mjs:2568:18)
    at callHooks (core.mjs:2527:17)
    at executeInitAndCheckHooks (core.mjs:2478:9)
    at refreshView (core.mjs:9525:21)

The console.log indicates that when called, the control inputABC123 (along with as many inputs as there are books) does exist. It shows up before the errors. Thinking the page was trying to load the data before the script had the time to set them as controls, I added the ngIf="myBookFG.contains('input'+book.isbn)", so that it wouldn't load them unless the control already existed, but even with that, the error still exists. Also, interacting with elements from the page (clicking on selects, expanding panels, etc), will load a few more books onto the list, without reloading the page. [which means that if I interact with enough elements from the page, I will end up with a functional form - not that I could ask the user to click on a button 25 times to see a working page]

I suspect this is an issue that has to do with asynchronous data, but I cannot seem to pinpoint how to fix this. Removing formControlName='input{{book.isbn}} from the input allows all the data to load correctly, but without this, I'll be unable to get the selected amount from every input to calculate the sum of books to order. (and I need to compare this sum with a different input on the form for validation purpose, as well as getting for each book the amount entered)


Solution

  • Here's an update with the solution.

    Turns out all was missing was a formGroupName.

    <form name="form" [formGroup]="mainForm">
        <ng-template ngFor let-book [ngForOf]='datas.books'>
            <div formGroupName="myBookFG">
                <span> {{book.title}}</span>
            </div>
            <div *ngIf="myBookFG.contains('input'+book.isbn)">
                <mat-form-field>
                    <mat-label>Amount to order</mat-label>
                    <input matInput maxlength='16'formControlName='input{{book.isbn}}' >
                </mat-form-field>
            </div>
        </ng-template>
    </form>
    

    Without that tag, the interpreter had no way of knowing it was within the group I had defined, and couldn't find the inner controls of that group.