Reactive Forms

杨旭 bio photo By 杨旭

Reactive Forms

Reactive forms is an Angular technique for creating forms in a reactive style. see: https://angular.cn/docs/ts/latest/guide/reactive-forms.html

Two different form-building ways: template-driven and reactive (model-driven). They belong to two module: ReactiveFormsModule and FormsModule.

Reactive forms offer the ease of using reactive patterns, testing, and validation

You create and manipulate form control objects directly in the component class. Rather than update the data model directly, the component extracts user changes and forwards them to an external component or service, which does something with them (such as saving them) and returns a new data model to the component that reflects the updated model state.

Template-driven forms

  • Don’t create form control objects.
  • Angular create them and we use them with ngModel.
  • Template-driven forms as asynchronous, which may complicate development.

Async vs. sync

  • Reactive forms are synchronous.
  • Template-driven forms are asynchronous.

In reactive forms, the whole entrie form control tree is created in code, we can update the value through the tree.

In template forms, form controls are delegated to directives.

To avoid “changed after checked” errors, these directives take more than one cycle to build the entire control tree. That means you must wait a tick before manipulating any of the controls from within the component class.

When using unit tests, template-driven forms must wrap tests with async and fakeAsync. But in reactive forms, everything is available when you need.

Reactive Form Demo

Create a data-model.ts to hold the models.

We create models that we are going to use.

export class Hero {
  id = 0;
  name = '';
  addresses: Address[];
}

export class Address {
  street = '';
  city   = '';
  state  = '';
  zip    = '';
}

export const heroes: Hero[] = [
  {
    id: 1,
    name: 'Whirlwind',
    addresses: [
      {street: '123 Main',  city: 'Anywhere', state: 'CA',  zip: '94801'},
      {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'},
    ]
  },
  {
    id: 2,
    name: 'Bombastic',
    addresses: [
      {street: '789 Elm',  city: 'Smallville', state: 'OH',  zip: '04501'},
    ]
  },
  {
    id: 3,
    name: 'Magneta',
    addresses: [ ]
  },
];

export const states = ['CA', 'MD', 'OH', 'VA'];

Then we create a simple form component, import ReactiveFomrsModule

@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule
  ],
  declarations: [
    HeroDetailComponent
  ]
})
export class DemoComponentsModule { }
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.scss']
})
export class HeroDetailComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

Create a FormControl object to bind to an input element.

  name = new FormControl();

A FormControl constructor accepts three, optional arguments: the initial data value, an array of validators, and an array of async validators. see: https://angular.io/docs/ts/latest/cookbook/form-validation.html

<div class="container form-group">
  <label class="center-block">Name:</label>
  <input class="form-control" [formControl]="name">
</div>

Usually, there are multiple FormControls in a form, we register them in a FromGroup

export class HeroDetailComponent {
  heroForm = new FormGroup({
    name: new FormControl()
  });
}

The FormGroup need to be reflected in the template.

<div class="container">
  <form [formGroup]="heroForm" novalidate>
    <div class="form-group">
      <label class="center-block">Name:</label>
      <input class="form-control" formControlName="name">
    </div>
  </form>
</div>
  • Bind the form to FormGroup with [formGroup]="heroForm".
  • novalidate means that the browser will skip native validations.
  • When working with formGroup, we need to use formControlName="name" to associated with the FormControl in formGroup.

We will check the form status and values with heroForm property.

<p>Form value: </p>
<p>Form status: </p>

When entering the hero’s name the form’s value changed.

Reducing repetition and clutter with FormBuilder

export class HeroDetailComponent implements OnInit {

  heroForm: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
    this.heroForm = this.fb.group({
      name: ''
    });
  }
}

Creating FormGroup with FormBuilder is much more simple and do the same thing with the new statements.

We create FormControl with the key and an empty string without any validations.

As a new feature, the hero’s name should be required.

Check the input value with Validators.required.


With the required validator, the form’s status will be INVALID it the name is empty.

Status will be valid if we enter a name.

see Form Validations for details: https://angular.io/docs/ts/latest/cookbook/form-validation.html

Fill the formGroup and template to show hero detail.

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { states } from 'app/demo-components/react-form/data-model';

@Component({
  selector: 'hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.scss']
})
export class HeroDetailComponent implements OnInit {

  heroForm: FormGroup;
  states = states;

  constructor(private fb: FormBuilder) { }

  ngOnInit(): void {
    this.heroForm = this.fb.group({
      name: ['', Validators.required],
      street: '',
      city: '',
      state: '',
      zip: '',
      power: '',
      sidekick: ''
    });
  }
}
<div class="container">
  <form [formGroup]="heroForm" novalidate>
    <div class="form-group">
      <label class="center-block">Name:</label>
      <input class="form-control" formControlName="name">
    </div>
    <div class="form-group">
      <label class="center-block">Street:
        <input class="form-control" formControlName="street">
      </label>
    </div>
    <div class="form-group">
      <label class="center-block">City:
        <input class="form-control" formControlName="city">
      </label>
    </div>
    <div class="form-group">
      <label class="center-block">State:
        <select class="form-control" formControlName="state">
            <option *ngFor="let state of states" [value]="state"></option>
        </select>
      </label>
    </div>
    <div class="form-group">
      <label class="center-block">Zip Code:
        <input class="form-control" formControlName="zip">
      </label>
    </div>
    <div class="form-group radio">
      <h4>Super power:</h4>
      <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
      <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
      <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
    </div>
    <div class="checkbox">
      <label class="center-block">
        <input type="checkbox" formControlName="sidekick">I have a sidekick.
      </label>
    </div>
  </form>
</div>

<p>Form value: </p>
<p>Form status: </p>

Pay attention to the formGroupName and formControlName attributes. They are the Angular directives that bind the HTML controls to the Angular FormGroup and FormControl properties in the component class.

The form is too big and we should manage them with Nasted FormGroup

Create nested form group with FormBuilder.

    this.heroForm = this.fb.group({
      name: ['', Validators.required],
      address: this.fb.group({
        street: '',
        city: '',
        state: '',
        zip: ''
      }),
      power: '',
      sidekick: ''
    });

Wrap the address with div and add a formGroupName directive to bind the address.

<div formGroupName="address" class="well well-lg">
  <!-- address list -->
</div>

We can inspect the FormControl with formGroup and get() method.

<p>Name value: </p>

Populate the form model with setValue and patchValue

Previously you created a control and initialized its value at the same time. You can also initialize or reset the values later with the setValue and patchValue methods.

form.setValue

When using form.setValue, it will assign every control as once. It will check the parameter’s structure and return helpful error message.

But the patchValue will fail silently.

form.patchValue

With patchValue you can assign specified control’s value with an object of key/value pairs.

When to set form model values (ngOnChanges)

As if the form is editing a hero which is an input property that binded by the parent component. When the parent component have changed the value, the formGroup need to be updated by setValue method inside ngOnChanges() hook.

We can reset the form’s status and value with formGroup.reset() method.

The reset method accepts an optional state parameter that specify the state to be set. It will use setValue method to change state and controls.

ngOnChanges() {
  this.heroForm.reset({
    name: this.hero.name,
    address: this.hero.addresses[0] || new Address()
  });
}

Use FormArray to present an array of FormGroups

FormGroup and FormControl only hold fixed number of properties. FormArray supports an arbitrary number of controls or groups.

A hero can have many lairs.

Change the formGroup from address to secret lairs

this.heroForm = this.fb.group({
  name: ['', Validators.required],
  secretLairs: this.fb.array([])
});
  • Changing the form model's property name is an important point: the form model doesn’t have to match the data model.

Set the secretLaires

const addresses = [
  {street: '123 Main',  city: 'Anywhere', state: 'CA',  zip: '94801'},
  {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'},
];

// map model array to FormGroup array.
const addressesGroups = addresses.map(address => this.fb.group(address));

// create FormArray by build method and FormGroup[]
const addressFormArray = this.fb.array(addressesGroups);

// override the controller with setControl
this.heroForm.setControl('secretLairs', addressFormArray);
  • Notice that setControl method will override the control, instead of setValue method which will update the control’s value.
  • Notice also that the secretLairs FormArray contains FormGroups, not Addresses.

Displaying the FormArray

  <div formArrayName="secretLairs" class="well well-lg">
    <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
      <!-- The repeated address template -->
    </div>
  </div>
  • With formArrayName directive.
  • secretLairs access in *ngFor directive will call a getter method.
  get secretLairs() {
    return this.heroForm.get('secretLairs') as FormArray;
  }
  • formGroupName in the loop bind to the index in the array.

When adding a new address, we need to push a FormGroup object.

addLair() {
  this.secretLairs.push(this.fb.group(new Address()));
}

Observe control changes

Angular calls ngOnChanges when the input hero is changed, but Angular does not call it when the hero’s property is changed.

We can learn about those changes with subscribing to one of the form control.

nameChangeLog: string[] = [];
logNameChange() {
  const nameControl = this.heroForm.get('name');
  nameControl.valueChanges.forEach(
    (value: string) => this.nameChangeLog.push(value)
  );
}