Angular
Forms
Form Types
- Template-driven
- Model-driven (reactive)
Template-driven forms
- Implicit and mutable state management
- Asynchronous
- FormsModule
-
Template is the source of truth
Model-driven (reactive) forms
- Explicit and immutable state management
- Synchronous
- ReactiveFormsModule
-
Component is the source of truth
Under the hood
Abstract
Control
FormGroup
FormControl
FormArray
<form>
<label>Name: </label>
<input />
<label>Age: </label>
<input />
</form>
Phase 1: Basic Form
Template-driven form
Component
Template
import { Component } from '@angular/core';
@Component ({
selector: 'TemplateDrivenForm',
templateUrl: './TemplateDrivenForm.html'
})
export default class {
user = {
age: null,
name: ''
}
}
<form>
<label>Name: </label>
<input [(ngModel)]="user.name" name="name" />
<label>Name: </label>
<input [(ngModel)]="user.age" name="age" />
</form>
Model-driven(reactive) form
Component
Template
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms'
@Component ({
selector: 'ModelDrivenForm',
templateUrl: './ModelDrivenForm.html'
})
export default class {
name = new FormControl();
age = new FormControl();
}
<form>
<label>Name: </label>
<input [formControl]="name" />
<label>Name: </label>
<input [formControl]="age" />
</form>
Updating State
import { Component } from '@angular/core';
@Component ({
selector: 'TemplateDrivenForm',
templateUrl: './TemplateDrivenForm.html'
})
export default class {
user = {
age: null,
name: ''
}
submit() {
this.user = {
name: 'Dohn Joe',
age: 27
};
}
}
import { Component } from '@angular/core';
import {
FormControl,
FromGroup
} from '@angular/forms'
@Component ({
selector: 'ModelDrivenForm',
templateUrl: './ModelDrivenForm.html'
})
export default class {
user = new FromGroup({
name: new FormControl(''),
age: new FormControl('')
})
submit() {
this.user.setValue({
name: 'John Doe',
age: 27
});
}
}
Model-driven (reactive)
Template-driven
<form>
<label>Name: </label>
<input />
<label>Age: </label>
<input />
<h1>Contact Info</h1>
<div>
<label>Type:</label>
<select>
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input/>
</div>
</form>
Phase 2
Template-driven form
Component
Template
import { Component } from '@angular/core';
@Component ({
selector: 'TemplateDrivenForm',
templateUrl: './TemplateDrivenForm.html'
})
export default class {
user = {
age: null,
name: '',
phone: {
type: '',
number: ''
}
}
}
<form>
<label>Name: </label>
<input [(ngModel)]="user.name" name="name"/>
<label>Age: </label>
<input [(ngModel)]="user.age" name="age" />
<h1>Contact Info</h1>
<div ngModelGroup="phone">
<label>Type:</label>
<select [(ngModel)]="user.phone.type" name="type">
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input [(ngModel)]="user.phone.number" name="number" />
</div>
</form>
Model-driven(reactive) form
Component
Template
import { Component } from '@angular/core';
import {
FormControl,
FromGroup
} from '@angular/forms'
@Component ({
selector: 'ModelDrivenForm',
templateUrl: './ModelDrivenForm.html'
})
export default class {
user = new FromGroup({
name: new FormControl(),
age: new FormControl(),
phone: new FormGroup({
type: new FormControl(),
number: new FormControl()
})
});
}
<form [formGroup]="user">
<label>Name: </label>
<input formControlName="name" />
<label>Age: </label>
<input formControlName="age" />
<h1>Contact Info</h1>
<div formGroupName="phone">
<label>Type:</label>
<select formControlName="type">
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input formControlName="number" />
</div>
</form>
<form>
<label>Name: </label>
<input />
<label>Age: </label>
<input />
<h1>Contact Info</h1>
<div>
<label>Type:</label>
<select>
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input />
</div>
<button>Add</button>
</form>
Phase 3: Dynamic Form
Template-driven form
Component
import { Component } from '@angular/core';
@Component ({
selector: 'TemplateDrivenForm',
templateUrl: './TemplateDrivenForm.html'
})
export default class {
user = {
age: null,
name: '',
phones: [
this.createPhone()
]
}
get phones() { return this.user.phones }
createPhone() {
return {
type: '',
number: ''
}
}
addPhone() { this.phones.push(this.createPhone()); }
}
Template-driven form
Template
<form>
<label>Name: </label>
<input [(ngModel)]="user.name" name="name" />
<label>Age: </label>
<input [(ngModel)]="user.age" name="age" />
<h1>Contact Info</h1>
<div ngModelGroup="phones" *ngFor="let phone of phones; let i = index">
<div [ngModelGroup]="i">
<label>Type:</label>
<select [(ngModel)]="phone.type" name="type">
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input [(ngModel)]="phone.number" name="number" />
</div>
</div>
<button (click)="addPhone()">Add</button>
</form>
Model-driven(reactive) form
Component
import { Component } from '@angular/core';
import { FormControl, FromGroup, FormArray } from '@angular/forms'
@Component ({
selector: 'ModelDrivenForm',
templateUrl: './ModelDrivenForm.html'
})
export default class {
user = new FromGroup({
name: new FormControl(),
age: new FormControl(),
phones: new FormArray([
this.createPhone()
])
});
get phones() { return this.user.get('phones') as FormArray; }
createPhone() {
return new FormGroup({
type: new FormControl(),
number: new FormControl()
})
}
addPhone() { this.phones.push(this.createPhone()); }
}
Model-driven(reactive) form
Template
<form [formGroup]="user">
<label>Name: </label>
<input formControlName="name" />
<label>Age: </label>
<input formControlName="age" />
<h1>Contact Info</h1>
<div formArrayName="phones" *ngFor="let phone of phones.controls; let i = index">
<div [formGroupName]="i">
<label>Type:</label>
<select formControlName="type">
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input formControlName="number" />
</div>
</div>
<button (click)="addPhone()">Add</button>
</form>
Form Validation
Built-in Validators in Template-driven forms
<form>
<label>Name: </label>
<input [(ngModel)]="user.name" name="name" required/>
<label>Age: </label>
<input [(ngModel)]="user.age" name="age" />
<h1>Contact Info</h1>
<div ngModelGroup="phones" *ngFor="let phones of phones; let i = index">
<div [ngModelGroup]="i">
<label>Type:</label>
<select [(ngModel)]="user.type" name="type">
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input [(ngModel)]="user.number" name="number" />
</div>
</div>
<button (click)="addPhone()">Add</button>
</form>
Built-in Validators in Model-driven (reactive) forms
import { Component } from '@angular/core';
import { FormControl, FromGroup, FormArray, Validators } from '@angular/forms'
@Component ({
selector: 'ModelDrivenForm',
templateUrl: './ModelDrivenForm.html'
})
export default class {
user = new FromGroup({
name: new FormControl('', [ Validators.required ]),
age: new FormControl(),
phones: new FormArray([
this.createPhone()
])
});
get phones() { return this.user.get('phones') as FormArray; }
createPhone() {
return new FormGroup({
type: new FormControl(),
number: new FormControl()
})
}
addPhone() { this.phones.push(this.createPhone()); }
}
Custom Validator
import { AbstractControl } from '@angular/forms';
export default (control: AbstractControl): { [key: string]: any } | null => {
return Number.isInteger(+control.value) ? null: { noInteger: { value: control.value } };
}
import { Directive, Input } from '@angular/core';
import { NG_VALIDATORS, Validator } from '@angular/forms';
import isInteger from '../isIntegerValidator';
@Directive({
selector: '[isInteger]',
providers: [{ provide: NG_VALIDATORS, useExisting: isIntegerValidator, multi: true }]
})
export default class isIntegerValidator implements Validator {
@Input() isInteger: string;
validate = isInteger;
}
Custom Validators in Template-driven forms
<form>
<label>Name: </label>
<input [(ngModel)]="user.name" name="name" required/>
<label>Age: </label>
<input [(ngModel)]="user.age" name="age" required isInteger/>
<h1>Contact Info</h1>
<div formArrayName="phone" *ngFor="let phones of phones; let i = index">
<div [ngModelGroup]="i">
<label>Type:</label>
<select [(ngModel)]="user.type" name="type">
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input [(ngModel)]="user.number" name="number" />
</div>
</div>
<button (click)="addPhone()">Add</button>
</form>
import { Component } from '@angular/core';
import { FormControl, FromGroup, FormArray, Validators } from '@angular/forms'
import isIntegerValidator from '../isIntegerValidator';
@Component ({
selector: 'ModelDrivenForm',
templateUrl: './ModelDrivenForm.html'
})
export default class {
user = new FromGroup({
name: new FormControl('', [ Validators.required]),
age: new FormControl('', [ Validators.required, isIntegerValidator]),
phones: new FormArray([
this.createPhone()
])
});
get phones() { return this.user.get('phones') as FormArray; }
createPhone() {
return new FormGroup({
type: new FormControl(),
number: new FormControl()
})
}
addPhone() { this.phones.push(this.createPhone()); }
}
Custom Validators in Model-driven (reactive) forms
Control status CSS classes
- .ng-valid
- .ng-invalid
- .ng-pending
- .ng-pristine
- .ng-dirty
- .ng-untouched
- .ng-touched
Sync
Functions that take a control instance and immediately return either a set of validation errors or null. Can be passed as the third argument when FormControl is initiated.
Async
Functions that take a control instance and return a Promise or Observable that later emits a set of validation errors or null. Can be passed as the third argument when FormControl is initiated.
Model-driven (Reactive)
Form Builder
Provides syntactic sugar that shortens creating instances of a FormControl, FormGroup or FormArray. Reduces the amount of boilerplate needed to build complex forms.
Form Builder Methods
- group() returns FormGroup
- control() returns FormControl
- array() return FormArray
Form Builder
Component
import { Component, onInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import isIntegerValidator from '../isIntegerValidator';
@Component ({
selector: 'ModelDrivenForm',
templateUrl: './ModelDrivenForm.html'
})
export default class implements onInit{
public userForm: FormGroup;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.userForm = this.formBuilder.group({
name: ['', Validators.required],
age: ['', [ Validators.required, isIntegerValidator]],
phones: this.formBuilder.array([
this.createForm()
])
});
}
get phones() { return this.userForm.get('phones') as FormArray; }
createPhone() {
return new FormGroup({
type: new FormControl(),
number: new FormControl()
})
}
addPhone() { this.userForm.phones.push(this.createPhone()); }
}
Form Builder
Template
<form [formGroup]="user">
<label>Name: </label>
<input formControlName="name" />
<label>Age: </label>
<input formControlName="age" />
<h1>Contact Info</h1>
<div formArrayName="phone" *ngFor="let phones of phones.controls; let i = index">
<div [formGroupName]="i">
<label>Type:</label>
<select formControlName="type">
<option value="home">Home</option>
<option value="work">Work</option>
</select>
<label>Number:</label>
<input formControlName="number" />
</div>
</div>
<button (click)="addPhone()">Add</button>
</form>
Useful links
Thank you!
Angular. Forms
By Pavel Razuvalau
Angular. Forms
- 822