Search code examples
angularangular-reactive-formsangular-formsformarray

Angular - How to keep track of array of arrays in a FormGroup


I'm trying to create a form that contains a list of select controls (just for the sake of simplicity).

I'm not going to add new selects to the form, I just want to keep track of the selected option in each of the select controls. enter image description here enter image description here This is the json I use to generate the select controls:

    sports = [
    {
      sportId: '1',
      desc: 'Football',
      categories: [
        {
          categId: '1',
          desc: 'U-18'
        },
        {
          categId: '1',
          desc: 'U-20'
        },
        {
          categId: '2',
          desc: 'U-23'
        }
      ]
    },
    {
      sportId: '2',
      desc: 'Volleyball',
      categories: [
        {
          categId: '3',
          desc: 'Junior'
        },
        {
          categId: '4',
          desc: 'Youth'
        }
      ]
    },
    {
      sportId: '3',
      desc: 'Tennis',
      categories: [
        {
          categId: '5',
          desc: 'Singles'
        },
        {
          categId: '6',
          desc: 'Doubles'
        }
      ]
    }
  ];

And this is the html of my form

<form [formGroup]="registrationForm"(submit)="save()">
          <div class="form-group">
            <label for="firstname">First Name</label>
            <input
              type="text"
              class="form-control"
              id="firstname"
              formControlName="firstName"
              placeholder="First Name"
            />
          </div>
          <div class="form-group">
            <label for="lastname">Last Name</label>
            <input
              type="text"
              class="form-control"
              formControlName="lastName"
              id="lastname"
              placeholder="Last Name"
            />
          </div>
          <div class="form-group" *ngFor="let sport of sports; let i = index">
            <label attr.for="sport_{{i}}">{{sport.desc}}</label>
            <select class="form-control" id="sport_{{i}}">
                <option value="0">Select one</option>
                <option *ngFor="let cat of sport.categories" [value]="cat.categId">{{cat.desc}}</option>               
              </select>
          </div>
          <button type="submit" class="btn btn-primary">Submit</button>
        </form>

Building the form for the firstName and lastName is straightforward:

  constructor(private fb: FormBuilder) {}
  ngOnInit(): void {
    this.registrationForm = this.fb.group({
      firstName: '',
      lastName: ''
    });
  }

But I'm not sure about how to keep track of the selected options on each of the different select controls.

I'd really appreciate it if you could help me here.

EDIT

Here's a demo

https://stackblitz.com/edit/angular-xxsvaw


Solution

  • Each control that you want to keep track of could have an entry in the Form Group:

      constructor(private fb: FormBuilder) {}
      ngOnInit(): void {
        this.registrationForm = this.fb.group({
          firstName: '',
          lastName: '',
          // <- Each additional control should be listed here
          football: '', 
          volleyball: '',
          tennis: ''
        });
      }
    

    Think of the Form Group as the set of data you want to keep track of. So if you said you were not interested in dynamically adding sports, just keeping track of their values, then each of their values becomes an entry in the Form Group.

    You can find a "brute force" style solution here: https://stackblitz.com/edit/angular-i7ntj3

    If you want to dynamically add the controls (even though you don't plan to add more), you can use a FormArray as shown in your example. It looks like you were primarily missing the [formControlName] property and the code populating the FormArray.

    Component

      registrationForm: FormGroup;
    
      get categoryOptions(): FormArray {
        return <FormArray>this.registrationForm.get('options');
      }
    
      constructor(private fb: FormBuilder) { }
    
      ngOnInit(): void {
        this.registrationForm = this.fb.group({
          firstName: '',
          lastName: '',
          options: this.fb.array([])
        });
    
        // Add the sports to the FormArray
        this.sports.forEach(s =>
          this.categoryOptions.push(new FormControl()));
      }
    
      save() {
        console.log(JSON.stringify(this.registrationForm.value));
      }
    

    NOTE: The FormGroup needs to contain all of the form controls you want to track. In the above example, we track two text fields and a FormArray. A FormArray contains either a set of Form Controls or a set of Form Groups. And since those Form Array elements are not being added dynamically, we need to create them manually. That's the purpose of the forEach in the code above.

    Template

          <div formArrayName="options">
            <div *ngFor="let sport of sports; let i=index">
              <label attr.for="i">{{sport.desc}}</label>
              <select class="form-control" id="i" [formControlName]="i">
                <option value="0">Select one</option>
                <option *ngFor="let cat of sport.categories" [value]="cat.categId">{{cat.desc}}
                </option>
              </select>
            </div>
          </div>
    

    NOTE: The formArrayName must be set to the name of the FormArray as defined in the FormGroup. The ngFor loops through each sport and sets up an index that will map to each element of the array. The formControlName for each select element is then set to the array index.

    You can find this FormArray solution here: https://stackblitz.com/edit/angular-reactive-select-deborahk

    Hope this helps.