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