1
votes

I am using Reactive Forms FormGroup, FormBuilder and FormArray. I have a FormArray in which I would like to loop through an external array, create a list of checkboxes and then pass the id of those checkboxes to my FormArray.

My components typescript looks like:

export class AppComponent  {
  form: FormGroup;

  sites = [ "site-1", "site-2", "site-3", "site-4" ];

  constructor(private fb: FormBuilder){ 
  }

  ngOnInit() {
    this.form = this.fb.group({
      type: [ '', Validators.required ],
      sites: this.fb.array([
        this.fb.group({
          siteId: [ '', Validators.required ]
        })
      ])
    })
  }
}

And my html...

<form [formGroup]="form">

  <input type="checkbox" formControlName="type" id="type">
  <label for="type">Type</label>

  <div class="checkbox-group" style="margin-top:40px">
      <div class="checkbox-wrap" id="productsContainer">
          <input type="checkbox" id="all">
          <label for="all">All mills</label>
          <div *ngFor="let site of sites; let i = index">
              <input type="checkbox" [id]="i" /> 
              <label [for]="i"> {{ site }} </label>
          </div>
      </div>
  </div>
</form>

I know I need to pass the formArrayName into the html but I am getting console errors when trying to pass in the formArrayName="sites" and then using the "i" from my loop as the formGroupName. What am I doing wrong?

Here's a StackBlitz with the full setup...

https://stackblitz.com/edit/angular-rcfnwi?file=app%2Fapp.component.html

2
Would this work for you? stackblitz.com/edit/angular-sgijvp?file=app%2Fapp.component.ts Here it's not taken into consideration with the all option, but that is also not in your question :P :PAJT82
Yes that is perfect, thank you!JordanBarber

2 Answers

3
votes

You need to populate the FormArray with the individual FormControls that correspond to the items in your sites list. In my fork example, I did this in the initialization of the FormGroup (https://stackblitz.com/edit/angular-hilz9h) by mapping over the sites array and generating FormControls with a default value of false (unchecked). I also removed the embedded FormGroup that was surrounding the sites FormArray to make it simpler. I then added the formArrayName directive to the wrapping div with the string attribute corresponding to the 'sites' name of the FormArray from the ts file. Finally, I added a [formControlName] directive and passed it the ngFor index of each input to correspond to the array index of the FormArray controls. FormArrays internally keep track of their controls by index.

So, your ngOnInit now looks like:

ngOnInit() {
    this.form = this.fb.group({
      type: [ '', Validators.required ],
      sites: this.fb.array(this.sites.map(s => this.fb.control(false)),
          [ Validators.required ])
    })
  }

and your html file now looks like:

<form [formGroup]="form">

  <input type="checkbox" formControlName="type" id="type">
  <label for="type">Type</label>

  <div class="checkbox-group" style="margin-top:40px">
      <div class="checkbox-wrap" id="productsContainer" formArrayName="sites">
          <input type="checkbox" id="all">
          <label for="all">All mills</label>
          <div *ngFor="let site of sites; let i = index">
              <input type="checkbox" [id]="i" [formControlName]="i"/> 
              <label [for]="i"> {{ site }} </label>
          </div>
      </div>
  </div>
</form>

<pre> {{ form.value | json }}</pre>
-1
votes

Really the answer work, but, in my opinion is not clearly a Form Array. When we have a serie of checks, we must to understand that there are two separate concept: the form and the data. Yes this two concepts are not necesary equal.

I choose create the form like

//we create a FormGroup array transform the array "this.sites"
const controls:FormGroup[]=this.sites.map(x=>{
  return this.fb.group({
    site:false
  })
});
//in this.form I use this array of FormGroup
this.form = this.fb.group({
  type: ['', Validators.required],
  sites: this.fb.array(controls)
})

Well, when we make the form, the form.value can be like

{
  "type": "", 
  "sites": [ 
    { "site": false }, { "site": true }, { "site": false }, { "site": true }
  ]
} 

Then, in submit we must transform the form.value to a data we like

  submit(form: FormGroup) {
    if (form.valid) {
      //create an array of sites
      let sites: string[] = [];
      let i: number = 0;
      form.value.sites.forEach((x:any) => {
        if (x.site)
          sites.push(this.sites[i]);
        i++;
      });
      //Our data will be
      let data = {
        type: form.value.type,
        sites: sites
      }
      console.log(data);
    }
  }

Finally, the form:

<form [formGroup]="form" (submit)="submit(form)" novalidate">
<input formControlName="type">
  <div formArrayName="sites" *ngFor="let site of form?.get('sites').controls;let i=index">
    <div [formGroupName]="i">
      <--!see that formControlName is "site"
      and the labels of the check use the array this.sites -->
      <input type="checkbox" formControlName="site" /> {{sites[i]}}
    </div>
  </div>
  <button>send</button>
</form>

All are asking Why on earth and What is it? All the things must be too complex? My answer is that it is not so complex. FurtherMore, imagine how can populate the form with data.

//if data={type:"a",sites:["site-2","site-4"], we create the form like
const controls:FormGroup[]=this.sites.map(x=>{
  return this.fb.group({
    site:data.sites.indexOf(x)>=0; //true or false
  })
});
this.form = this.fb.group({
  type: [data.type, Validators.required],
  sites: this.fb.array(controls)
})