Iterating through a FormArray containing FormGroups with *ngFor
Asked Answered
J

2

12

In Ionic 2 I am trying to create a dynamic form which shall display a list of toggle buttons.

To do so I am trying to use a FormArray and relied on the Angular doc and mainly on this post

Based on this, I implemented the following

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

    <ion-list>

      <!-- Personal info -->
      <ion-list-header padding-top>
        Informations personnelles
      </ion-list-header>
      <ion-item>
        <ion-label stacked>Prénom</ion-label>
        <ion-input formControlName="firstname" [value]="(user | async)?.info.firstname" type="text"></ion-input>
      </ion-item>

      <!-- Sport info -->
      <ion-list-header padding-top>
        Mes préférences sportives
      </ion-list-header>
      <ion-list formArrayName="sports">

        <ion-item *ngFor="let sport of accountForm.controls.sports.controls; let i = index" [formGroupName]="i">
          <ion-label>{{sport.name | hashtag}}</ion-label>
          <ion-toggle formControlName="{{sport.name}}"></ion-toggle>
        </ion-item>

      </ion-list>

    </ion-list>


  </form>

Controller

ionViewDidLoad() {
    console.log('MyAccountPage#ionViewDidLoad');

    // Retrieve the whole sport list
    this.sportList$ = this.dbService.getSportList();
    this.sportList$.subscribe(list => {

      // Build form
      let sportFormArr: FormArray = new FormArray([]);

      for (let i=0; i < list.length; i++) {
        let fg = new FormGroup({});
        fg.addControl(list[i].id, new FormControl(false));
        sportFormArr.push(fg);
      }

      this.accountForm = this.formBuilder.group({
        firstname: ['', Validators.compose([Validators.maxLength(30), Validators.pattern('[a-zA-Z ]*'), Validators.required])],
        lastname: ['', Validators.compose([Validators.maxLength(30), Validators.pattern('[a-zA-Z ]*'), Validators.required])],
        company: [''],
        sports: sportFormArr
      });

      console.log('form ', this.accountForm);
    })

  }

But I get the following error:

ERROR Error: Cannot find control with path: 'sports -> 0 -> '

Here is the content of accountFormenter image description here

Any idea why ?

Jaclyn answered 1/6, 2017 at 13:24 Comment(5)
What are you expecting on this line? formControlName="{{sport.name}}". I bet the error is with this.Gynecocracy
Error remains and is now "sports -> 0 -> 0" because i is equal to the index. I need to display the control with its name which is dynamic and supposed to be in sport.nameJaclyn
How do you really want to that the formgroups inside sports look like? I mean like how would the object look like?Gosling
@AJT_82 I would like to have an array of formGroups. One formGroup per sport and within each sport formGroup have a formControl with a dynamic name (eg. golf, tennis...) and its associated toggled value (true or false). But maybe I am making it too complicated. What I would like in the end is a list of dynamic toggles (the sport list being dynamic) and when saving the form retrieve which sports have been toggled. Do I answer your question?Jaclyn
@ManuelRODRIGUEZ Yes you did answer my question :) And I think my answer may fill your requirements. Hopefully :DGosling
G
18

I don't how/if you can tap into getting the property name of of a dynamically created formcontrol... but you can utilize the list you have instead, from which you are building the formgroups. Then you just have to assign the list you are getting to a local variable, so that you can use it in template.

First off, if you want to use the name of the sport, you need to change your creation the formgroup and use name instead of id:

And I would restructure this a bit, use a getter for the formarray and do:

// choose better name ;)
get formArr() {
  return this.accountForm.get("sports") as FormArray;
}

fb refers to FormBuilder in this case:

this.accountForm = this.fb.group({
  sports: this.fb.array([])
});

// ...........

// don't use any, type your data!
this.list.forEach((item: any, i) => {
  this.formArr.push(
    this.fb.group({
      [this.list[i].name]: false
    })
  );
});

Then as said, you can utilize the list and the index in template, so:

 <ion-item *ngFor="let sport of formArr.controls; let i = index" [formGroupName]="i">
    <ion-label>{{list[i].name}}</ion-label>
    <ion-toggle [formControlName]="list[i].name"></ion-toggle>
 </ion-item>

Here's a STACKBLITZ with (only) angular

Gosling answered 1/6, 2017 at 14:25 Comment(5)
Thanks @AJT_82 , it works perfect. Only thing is that I have to be sure that the order is the same. No reason to be different but still I have to be sure of this, right?Jaclyn
Absolutely. But I simplified the code for the Stack post, I actually do set some toggles to true. I will be careful. Thanks again!Jaclyn
Thanks! Using accountForm.controls was the key for me; the controls property can be iterated. If you try to ngForOf with the FormArray directly you may see the message "Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays."Demonology
FYI, in Angular 9 with type checking properly enabled this will throw an error.Terminator
@JamesHancock, thanks for feedback! Old post, so I also updated the answer a bit just now. Hopefully the typechecking is ok now (not tested). Let me know if still causing issues :)Gosling
G
2

Another approach is to create a pipe which returns an abstractControl as a formArray

import { Pipe, PipeTransform } from '@angular/core';
import { AbstractControl, FormArray } from '@angular/forms';

@Pipe({
  name: 'asFormArray'
})
export class AsFormArrayPipe implements PipeTransform {
  transform(value: AbstractControl): FormArray {
    return value as FormArray;
  }
}

This can then be used in your template as follows:

<ul>
  <li *ngFor="let myControl of (myFormArray | asFormArray).controls">
    {{ myControl.value | json }}
  </li>
</ul>
Grahamgrahame answered 27/6, 2022 at 9:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.