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