Managing Nested and Dynamic Forms in Angular

Learn how to use FormArray to dynamically add and remove FormGroups and FormControls in reactive forms.

April 04, 2019

Angulars ReactiveForms give us immense capabilities with its robust API, but the learning curve can be a bit steep from plain old template-driven forms that many are used to. This quick guide will explain Angulars form elements and how to combine them, nest them, and dynamically create them in almost any scenario.

AbstractControl

First, it’s important to know about AbstractControl, the class extended across most of the form elements we’ll be working with. It has multiple properties that manage everything from the validity state to what the parent element may be, and methods that allow us to mark the state of the control(touched, untouched, dirty, etc), enable/disable the control, get the value, set the value, etc. There’s a lot going in this class, so it’s handy to have it’s documentation available to refer to:

abstract class AbstractControl {
  constructor(validator: ValidatorFn, asyncValidator: AsyncValidatorFn)
  value: any
  validator: ValidatorFn | null
  asyncValidator: AsyncValidatorFn | null
  parent: FormGroup | FormArray
  status: string
  valid: boolean
  invalid: boolean
  pending: boolean
  disabled: boolean
  enabled: boolean
  errors: ValidationErrors | null
  pristine: boolean
  dirty: boolean
  touched: boolean
  untouched: boolean
  valueChanges: Observable<any>
  statusChanges: Observable<any>
  updateOn: FormHooks
  root: AbstractControl
  setValidators(newValidator: ValidatorFn | ValidatorFn[]): void
  setAsyncValidators(newValidator: AsyncValidatorFn | AsyncValidatorFn[]): void
  clearValidators(): void
  clearAsyncValidators(): void
  markAsTouched(opts: { onlySelf?: boolean; } = {}): void
  markAsUntouched(opts: { onlySelf?: boolean; } = {}): void
  markAsDirty(opts: { onlySelf?: boolean; } = {}): void
  markAsPristine(opts: { onlySelf?: boolean; } = {}): void
  markAsPending(opts: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
  disable(opts: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
  enable(opts: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
  setParent(parent: FormGroup | FormArray): void
  abstract setValue(value: any, options?: Object): void
  abstract patchValue(value: any, options?: Object): void
  abstract reset(value?: any, options?: Object): void
  updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean; } = {}): void
  setErrors(errors: ValidationErrors, opts: { emitEvent?: boolean; } = {}): void
  get(path: string | (string | number)[]): AbstractControl | null
  getError(errorCode: string, path?: string | (string | number)[]): any
  hasError(errorCode: string, path?: string | (string | number)[]): boolean
}

FormControl

The basic element of building Angular forms is the FormControl. This is a class that represents that input element on a page with a name value you’re likely used to seeing. Any piece of information we want to collect in a form, whether it’s an input, select, dropdown, or custom element needs to have a representative FormControl. The [formControl] directive is used to bind the input element in the DOM to it’s respective FormControl.

import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'

@Component({
  selector: 'app-basic',
  template: `
    <input type="text" [formControl]="name">
  `
})
export class BasicComponent implements OnInit {
  public name = new FormControl('your name here');

  constructor() { }

  ngOnInit() {
  }

}

FormControls can be initialized with a value, like ‘your name here’ in the above example, and enabled/disables status, and set any validators necessary.

FormGroups

FormGroup is the class that allows us to group a number of controls together. It also extends the AbstractControl class, meaning we can track the validity and value of all the FormControls in a FormGroup together. This allows us to easily manage our form as a whole. The [formGroup] directive binds the FormGroup to a DOM element.

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

let emailRegex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$";

@Component({
  selector: 'app-formgroup',
  template: `
  <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
    <label>
      First name:
      <input type="text" formControlName="firstName">
    </label>
    <label>
      Last name:
      <input type="text" formControlName="lastName">
    </label>
    <label>
      Email:
      <input type="text" formControlName="email">
    </label>
    <button [disabled]="!userForm.valid" type="submit">submit</button>
  </form>
  `
})
export class FormgroupComponent implements OnInit {
  public userForm = new FormGroup({
    firstName: new FormControl('', {validators: Validators.required}),
    lastName: new FormControl('', {validators: Validators.required}),
    email: new FormControl('', {validators: Validators.pattern(emailRegex)})
  });

  constructor() { }

  ngOnInit() {
  }

  onSubmit() {
    console.log(this.userForm.value);
  }

}

FormArray

FormArray is a class that aggregates FormControls into an array, similar to FormGroup creating an object from FormControls. FormArrays can have controls pushed to them or removed from them similar to the way you’d manipulate an array in vanilla JS, and offer us a lot of power and flexibility when creating nested and dynamic forms.

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, FormArray, Validators } from '@angular/forms';

let emailRegex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$";

@Component({
  selector: 'app-formarray',
  template: `
  <form [formGroup]="usersForm" (ngSubmit)="onSubmit()">
    <ng-container *ngFor="let userFormGroup of usersForm.controls; let i = index">
      <div [formGroup]="userFormGroup">
        <label>
          First name:
          <input type="text" formControlName="firstName">
        </label>
        <label>
          Last name:
          <input type="text" formControlName="lastName">
        </label>
        <label>
          Email:
          <input type="text" formControlName="email">
        </label>
      </div>
    </ng-container>
    <button type="submit">console log form value</button>
  </form>
  `
})
export class FormarrayComponent implements OnInit {
  public usersForm = new FormArray([
    new FormGroup({
      firstName: new FormControl('user 1', {validators: Validators.required}),
      lastName: new FormControl('', {validators: Validators.required}),
      email: new FormControl('', {validators: Validators.pattern(emailRegex)})
    }),
    new FormGroup({
      firstName: new FormControl('user 2', {validators: Validators.required}),
      lastName: new FormControl('', {validators: Validators.required}),
      email: new FormControl('', {validators: Validators.pattern(emailRegex)})
    })
  ]);

  constructor() { }

  ngOnInit() {
  }

  onSubmit() {
    console.log(this.usersForm.value);
  }

}

In this example, we’re using an ngFor loop to iterate through userForm.controls, because userForm is a FormArray. In this FormArray are controls, so as we loop through the controls, we need to be sure to use the [formGroup] directive to bind each iteratee DOM element to it’s respective FormGroup instance.

FormBuilder

Repeatedly typing new FormControl(''), new FormGroup({}), and new FormArray([]) can become a bit tedious, especially when creating larger forms. Fortunately Angular has shorthand syntax we can use thanks to the FormBuilder class.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

let emailRegex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$";

@Component({
  selector: 'app-formbuilder',
  template: `
  <form [formGroup]="usersForm" (ngSubmit)="onSubmit()">
    <ng-container *ngFor="let userFormGroup of usersForm.controls.users.controls; let i = index">
      <div [formGroup]="userFormGroup">
        <label>
          First name:
          <input type="text" formControlName="firstName">
        </label>
        <label>
          Last name:
          <input type="text" formControlName="lastName">
        </label>
        <label>
          Email:
          <input type="text" formControlName="email">
        </label>
      </div>
    </ng-container>
    <button type="submit">console log form value</button>
  </form>
  `
})
export class FormbuilderComponent implements OnInit {
  public usersForm: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.usersForm = this.fb.group({
      date: this.fb.control(new Date()),
      users: this.fb.array([
        this.fb.group({
          firstName: [{value: 'user 1', disabled: false}, Validators.required],
          lastName: [{value: '', disabled: false}, Validators.required],
          email: [{value: '', disabled: false}, Validators.pattern(emailRegex)]
        }),
        this.fb.group({
          firstName: [{value: 'user 2', disabled: false}, Validators.required],
          lastName: [{value: '', disabled: false}, Validators.required],
          email: [{value: '', disabled: false},  Validators.pattern(emailRegex)]
        })
      ])
    })
  }

  onSubmit() {
    console.log(this.usersForm.value);
  }

}

Now that we have an understanding of the pieces we can build forms with, let’s look at building a more complex form example.

Creating and Removing FormControls & FormGroups Dynamically

Let’s say we want to create a form that allows a user to create an unlimited number of users. This form will need to be able to allow a user to add new FormGroups to create additional users as needed, as well as remove FormGroups they don’t want. We use a FormArray to hold a FormGroups of FormControls for each user we want to create. To do this, we can use FormArray methods:

  • An insert method that takes two parameters, the index at which to insert, and the control to insert.
  • A removeAt method, which takes the index of the control to remove.
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';

let emailRegex = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$";

@Component({
  selector: 'app-addformgroups',
  template: `
  <form [formGroup]="usersForm" (ngSubmit)="onSubmit()">
    <ng-container *ngFor="let userFormGroup of usersForm.controls.users.controls; let i = index">
      <div [formGroup]="userFormGroup">
        <label>
          First name:
          <input type="text" formControlName="firstName">
        </label>
        <label>
          Last name:
          <input type="text" formControlName="lastName">
        </label>
        <label>
          Email:
          <input type="text" formControlName="email">
        </label>
        <label>
          <button (click)="removeFormControl(i)">remove formGroup</button>
        </label>
      </div>
    </ng-container>
  </form>
  <button (click)="addFormControl()">add new user formGroup</button>

  `
})
export class AddformgroupsComponent implements OnInit {
  public usersForm: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.usersForm = this.fb.group({
      date: this.fb.control(new Date()),
      users: this.fb.array([
        this.fb.group({
          firstName: ['user 1', Validators.required],
          lastName: ['', Validators.required],
          email: ['', Validators.pattern(emailRegex)]
        }),
        this.fb.group({
          firstName: ['user 2', Validators.required],
          lastName: ['', Validators.required],
          email: ['',  Validators.pattern(emailRegex)]
        })
      ])
    })
  }

  removeFormControl(i) {
    let usersArray = this.usersForm.controls.users as FormArray;
    usersArray.removeAt(i);
  }

  addFormControl() {
    let usersArray = this.usersForm.controls.users as FormArray;
    let arraylen = usersArray.length;

    let newUsergroup: FormGroup = this.fb.group({
      firstName: ['', Validators.required],
      lastName: ['', Validators.required],
      email: ['', Validators.pattern(emailRegex)]
    })

    usersArray.insert(arraylen, newUsergroup);
  }

}

Now we have a form that dynamically adds and removes FormGroups to allow the user to create as many users as they would like. With a thorough understanding of FormControl, FormGroup, and FormArray we can create any form structure to meet our data submissions needs.

To see working examples of all the code snippets shown above, take a look at the repo on stackblitz.

https://stackblitz.com/github/tehfedaykin/angular-dynamic-forms