Angular2 – Handling forms: Reactive approach

Introduction to the Reactive Approach

  • Form is created programmatically and synchronized with the DOM

Reactive : Setup

  • In the app.module we need to comment out FormsModule and add ReactiveFormsModule

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    // FormsModule,
    HttpModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts

import { NgForm, FormGroup } from '@angular/forms';
...
  reactiveForm: FormGroup;

Reactive: Creating a form in code

  • Out intention now is bind an HTML form to a Form object created programmatically.

app.component.html

  <h2>Reactive forms</h2>
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <form>
        <div class="form-group">
          <label for="candidate">Candidate</label>
          <input type="text" id="candidate" class="form-control">
        </div>
        <div class="form-group">
          <label for="haircolor">Hair color</label>
          <select id="haircolor" type="select" class="form-control">
            <option value="blonde">Blonde</option>
            <option value="brown">Brown</option>
          </select>
        </div>

        <button
        class="btn btn-primary"
        type="submit"
        >Submit</button>
      </form>
    </div>
  </div>

app.component.ts

  //Reactive approach
  reactiveForm: FormGroup;

  ngOnInit() {
    this.reactiveForm = new FormGroup({
      'candidate': new FormControl(null),
      'haircolor': new FormControl('blonde')
    });
  }

Reactive: Syncing HTML and form

  • The first thing is tells Angular not to detect form tags and use my FormGroup created in Typescript. We use formGroup and formControlName built-in directives.

app.component.html

...
      <form [formGroup]="reactiveForm">
...
          <input type="text" id="candidate"
            class="form-control"
            formControlName="candidate">
...
          <select id="haircolor" type="select" class="form-control"
            formControlName="haircolor">

Reactive: Submitting the form

  • Here we can use ngSubmit as in TD forms , but we dont need any local reference to the form. We already have reference to the form in the Form object created.

app.component.html

<form [formGroup]="reactiveForm" (ngSubmit)="onSubmitReactive()">

app.component.ts

  onSubmitReactive() {
      console.log('submitted! reactive', this.reactiveForm);
  }

Reactive: Adding Validation

  • We will use Validators class to pass static references to functions to execute on validation.
  • We will use an array of Validators for making multiple validations.

app.component.ts

  ngOnInit() {
    this.reactiveForm = new FormGroup({
      'candidate': new FormControl(null, Validators.required),
      'email': new FormControl(null, [Validators.required,Validators.email]),
      'haircolor': new FormControl('brown')
    });
  }

Reactive: Getting access to controls

app.component.html

<span class="help-block" 
 *ngIf="!reactiveForm.get('candidate').valid && reactiveForm.get('candidate').touched">Please enter a valid candidate</span>

Reactive: Grouping controls

app.component.ts

  ngOnInit() {
    this.reactiveForm = new FormGroup({
      'userData': new FormGroup({
        'candidate':new FormControl(null, Validators.required),
        'email':new FormControl(null, [Validators.required,Validators.email])
      }),
      'haircolor': new FormControl('brown')
    });
  }

app.component.html

...
      <form [formGroup]="reactiveForm" (ngSubmit)="onSubmitReactive()">
        <div formGroupName="userData">
          <div class="form-group">
            <label for="candidate">Candidate</label>
            <input type="text" id="candidate"
              class="form-control"
              formControlName="candidate">
            <span class="help-block"
              *ngIf="!reactiveForm.get('userData.candidate').valid && reactiveForm.get('userData.candidate').touched">Please enter a valid candidate</span>
          </div>
          <div class="form-group">
            <label for="email-reactive">Email</label>
            <input type="text" id="email-reactive"
              class="form-control"
              formControlName="email">
          </div>

        </div>

Reactive: Array of Form controls (FormArray)

  • We will add hobbies dynamically as an input array.
  • We will use formArray directive in the HTML and FormArray in the typescript.

app.component.html

        <div formArrayName="hobbies">
          <h3>Your hobbies</h3>
          <button class="btn btn-primary" (click)="onAddHobby()">Add Hobby</button>
          <div class="form-group"
            *ngFor="let control of reactiveForm.get('hobbies').controls; let i = index">
            <input type="text" class="form-control" [formControlName]="i">
          </div>
        </div>

app.component.ts

 ngOnInit() {
 this.reactiveForm = new FormGroup({
 'userData': new FormGroup({
 'candidate':new FormControl(null, Validators.required),
 'email':new FormControl(null, [Validators.required,Validators.email])
 }),
 'hobbies': new FormArray([]),
 'haircolor': new FormControl('brown')
 });
 }
onAddHobby() {
    const control = new FormControl(null);
    (<FormArray>this.reactiveForm.get('hobbies')).push(control);

  }

Reactive: Creating Custom validators

  • We will check candidate against a  list of forbidden user names.

app.component.ts

    this.reactiveForm = new FormGroup({
      'userData': new FormGroup({
        'candidate':new FormControl(null, [Validators.required, this.forbiddenNameValidation.bind(this)]),
        'email':new FormControl(null, [Validators.required,Validators.email])
      }),
      'hobbies': new FormArray([]),
      'haircolor': new FormControl('brown')
    });
  }
...
  forbiddenNameValidation(control:FormControl): {[s: string]: boolean} {
      if(this.forbiddenUsernames.indexOf(control.value) !== -1) {
        return {'nameIsForbidden': true};
      }
      return null;  //Valid
  }

NOTE: It is important return null on validator function as it is how Angular recognizes validation is ok.

NOTE: We need to make bind(this) in the validators array for the custom validation for tells Angular to get the correct reference of the form control.

Reactive: Using Error Codes

  • We will customize error messages for custom validations.
  • We will check errors property in the form.controls object.

app.component.html

<div class="form-group">
            <label for="candidate">Candidate</label>
            <input type="text" id="candidate"
              class="form-control"
              formControlName="candidate">
            <span class="help-block"
              *ngIf="!reactiveForm.get('userData.candidate').valid && reactiveForm.get('userData.candidate').touched">
              <span class="help-block" *ngIf="reactiveForm.get('userData.candidate').errors['nameIsForbidden']">
                This name is forbidden
              </span>
              <span class="help-block" *ngIf="reactiveForm.get('userData.candidate').errors['required']">
                Name is required
              </span>
            </span>
          </div>

Reactive: Creating a Custom Async Validator

  • Sometimes we want to reach out a server to make a validation
  • So we need to make an async operation , in this case we can pass to the FormControl constructor a third parameter telling Async Validators.
  • In the example we simulate an async operation with a timeout and check if email is test@test.com
...
'email':new FormControl(null, [Validators.required,Validators.email], this.forbiddenEmails)
...

  forbiddenEmails(control:FormControl): Promise<any> | Observable<any> {
    const promise = new Promise<any>((resolve, reject) => {
    setTimeout(()=> {
      if(control.value === 'test@test.com') {
        resolve({'emailIsForbidden':true});
      } else {
        resolve(null);
      }
    },1500);
   });
   return promise;
 }

Reactive: Reacting to Status or Value Changes

  • We can monitor all the values and status changing in our form in every moment with two observables of the reactive forms: valueChanges and statusChanges.
  • We can hook on them:
  ngOnInit() {
    ...
    this.reactiveForm.valueChanges.subscribe(
      (value) => {console.log(value);}
    );
    this.reactiveForm.statusChanges.subscribe(
      (value) => {console.log(value);}
    );
  }

Reactive: Setting and Patching values

  • As in TD forms we can use setValue, patchValue and reset methods on the reactive form object.

Notes

Shortcut for property binding for strings
  • Usually property binding use single quotes:
    [pattern]="'*(0-9)'"
  • When we re using a string in a property binding we can type a simpler syntax:
    pattern="*(0-9)"
Patterns in template driven forms
  • We can add pattern matching in template driven forms with the HTML5 pattern.
  • For example we want numbers above 0 in this input field:
          <input
            type="text"
            id="amount"
            class="form-control"
            name="amount"
            ngModel
            required
            pattern="^[1-9]+[0-9]*$">

ds