Angular 2+ Reactive Form - Adding & Removing Form Controls

May 17, 2018

Complicated Problem 5: I need to dynamically add and remove form controls & form groups

Ok, let’s pretend we have an interface like so:

Ingredient Form

First we create our nested form.

this.recipeForm = this.fb.group({
  name: [null, Validators.required],
  description: [null],
  source: [null],
  category: [null],
  subcategory: [[]],
  //notice how we're passing a formbuilder array here
  ingredients: this.fb.array([]),
  calories: [null, [Validators.required, this.amountValidator]]
});

Next we’ll write the function to create a new nested form groups

public addIngredient() {
  const ingredientsFormArray = this.recipeForm.controls['ingredients'];
  //just your regular ole form group here, nothing new.
  const ingredientFormGroup = new FormGroup({
    name: new FormControl(null, [Validators.required]),
    amount: new FormControl(null, [Validators.required])
  });
  ingredientsFormArray.push(ingredientFormGroup);
}

We’ll also want to be able to remove dynamic form groups as well.

public removeIngredient(i) {
  const ingredientsFormArray = this.recipeForm.controls['ingredients'];
  ingredientsFormArray.removeAt(i);
}

When creating the markup, the most important thing with nested forms is remembering to pass the formgroup iteratee as the formgroup model.

<label for="ingredients">Ingredients</label>
<ng-container *ngFor="let ingredient of recipeForm.controls['ingredients'].controls; let i = index" >
  //passing our ingredient iteratee as our formgroup here
  <div [formGroup]="ingredient" class="row">
  //don't you forget about me!
    <div class="form-group col-sm">
      <label for="name">Name</label>
      <input class="form-control" formControlName="name" aria-describedby="name">
    </div>
    <div class="form-group col-sm">
      <label for="amount">Amount</label>
      <input class="form-control" formControlName="amount" aria-describedby="amount">
    </div>
    <div class="col-sm">
      <button class="btn btn-danger remove-ingredient" (click)="removeIngredient(i)">
        <fa-icon icon="times"></fa-icon>
      </button>
    </div>
  </div>
</ng-container>
<div class="row">
  <div class="col-sm">
    <button type="button" class="btn btn-success" (click)="addIngredient()">Add ingredient</button>
  </div>
</div>

But howtf do I validate dynamic forms???

Fear not, my young padawan, we can still iterate through our form controls and nested form arrays.

Object.keys(this.recipeForm.controls).forEach(field => {
  const control = this.recipeForm.get(field);

  if (!control['controls']) {
    control.markAsTouched({ onlySelf: true });
  }
  else {
    let nestedFormArray = control['controls'];
    nestedFormArray.forEach(subcCtrlGp => {
      Object.keys(subcCtrlGp['controls']).forEach(subField => {
        const nestedControl = subcCtrlGp.get(subField);
        nestedControl.markAsTouched({ onlySelf: true });
      });
    });
    //can extract this to a recursive function for deeply nested forms
  }
})

Demo code available at https://github.com/tehfedaykin/complicated-forms-app