Wednesday, October 5, 2016

Learning Angular 2: Exploring Reactive Form Classes and Validators

Version 0.0.6 of my sandbox GuildRunner Angular 2 application adds an example of using the reactive form and Validator classes provided by Angular 2.  The example was added to the sandbox collection of components rather than the main application, as I plan on taking what I learned from the exercise and expanding on it when I write the "real" forms.

In my blog post regarding GuildRunner release 0.0.4, which was my take on handling validation for Angular template-driven forms, I incorrectly stated that the other approach to forms supported by Angular 2 was referred to as "dynamic forms."  That's not the case:  the documentation page I was referring to was about how to dynamically generate form inputs for a collection of model data, which is a scenario where using the reactive form classes makes a lot of sense.  The documentation page that gave me the correct name to the alternative to template-driven forms - reactive forms - was the page on form validation.

In the reactive form style, you do not bind your form controls to your model data.  Instead, you bind them to the reactive form classes, which are:

The most basic reactive form - a form with a single input - would be constructed with a single FormGroup containing a single FormControl for the form input.  So a reactive form containing a single text input (say a "name" field with an initial value of "Bob") would be coded in the component like so:


/*
Import the reactive form classes (your Angular module will also have to import the Angular ReactiveFormsModule)
*/
import {FormControl, FormGroup } from '@angular/forms';
...
export class MyReactiveFormComponent implements OnInit {
  myForm: FormGroup;
  
  ngOnInit() {
    this.myForm = new FormGroup( {
      'name': new FormControl( 'Bob' )
    } )
  }

...And the HTML template for the form would be written like so:


<form [formGroup]="myForm">
  <input type="text" formControlName="name">
</form>

Note how the HTML form is connected to the FormGroup via the formGroup attribute, and how the text input is bound to the FormControl via the formControlName attribute: ngModel is not in play here.

Form input validation is applied by adding validator functions to the FormControl: a FormControl takes either a single validator function or an array of validator functions as its second constructor argument. Making the name input in our example required with a minimum length of 2 characters is a simple matter of adding the necessary validator functions shipped with Angular 2 within the Validators class:


  ngOnInit() {
    this.myForm = new FormGroup( {
      'name': new FormControl( 'Bob', [ Validators.required, Validators.minLength(2) ] )
    } )
  }

A validation check will occur anytime the value of the FormControl changes, whether that change is made via the UI or programmatically (which is an improvement over how the template-driven forms work).

A FormGroup can contain any number of FormControl objects.  It can also contain additional FormGroups (sub-groups within the main FormGroup) and FormArrays which hold a collection of unnamed, iterable FormControls.  A single validator function can be attached to each FormGroup and FormArray, usually a custom validator function that performs a validation based on multiple form values.

To try out these features, I created a form for updating certain properties of a Chapter domain class:

  • A text input for updating the chapter name.
  • A select box for selecting the guild the chapter belongs to.
  • A radio button for setting whether or not the chapter was the head chapter for the guild.
  • A series of checkboxes representing the defense measures used at the chapter location, represented in the Chapter domain class an array of defense measure ID values.
In the component, I created a single method (called in ngOnInit) for instantiating the reactive form classes and for subscribing to the change event emitter:

//sandbox/chapter-reactive-form/chapter-reactive-form.component.ts
import { Chapter } from "../../domain/chapter";
import { guilds } from "../../db/guilds";
import { defenseMeasures } from "../../db/defense-measures";

import {FormControl, FormGroup, FormArray, Validators, FormBuilder} from '@angular/forms';
...
export class ChapterReactiveFormComponent implements OnInit {

  chapter: Chapter;
  defenseArray: any = []; //Populated by ngOnInit with an array of defenses
  guildArray: any = []; //Populated by ngOnInit with an array of guilds
  defenseBoxArray: FormArray;
  form: FormGroup;
  ...
  constructor( private formBuilder: FormBuilder ) { }
  ...
  buildForm() {

    //Create a custom Validator function for the defenses array
    function hasDefenses( formArray: FormArray) {
      let valid = false;
      for( let c in formArray.controls ) {
        if( formArray.controls[c].value == true ) { valid = true }
      }
      return valid == true ? null : { noDefenses: true }
    }

    //Construct and populate the defenses FormArray outside of the FormBuilder so we can populate it dynamically
    this.defenseBoxArray = new FormArray( [], hasDefenses );
    for( let d in this.defenseArray ) {
      this.defenseBoxArray.push( new FormControl(( this.chapter.defenses.indexOf( this.defenseArray[d].id ) > -1 )))
    }

    this.form = this.formBuilder.group( {
      'name': [ this.chapter.name, [
        Validators.required,
        Validators.minLength(4),
        Validators.pattern('[a-zA-Z]+')
      ] ],
      'guild': [ this.chapter.guildId, Validators.required ],
      'headChapter': [ this.chapter.headChapter, Validators.required ],
      'defenses': this.defenseBoxArray
    } );

    this.form.valueChanges
      .subscribe( data => this.checkFormValidity( data ) );
  }

The hasDefenses() function definition is an example of how to create a custom validator function, which should either return null if validation passed or return an object literal that provides some context for why the validation failed. The function is then passed as the 2nd argument in the FormArray constructor.

The rest of the FormGroup representing the form is created using the FormBuilder, which provides a less verbose way to instantiating the other reactive form classes. The final statement in the method subscribes to the valueChanges event emitted by the form anytime a form value is updated and ties that event to the execution of the checkFormValidity method which I'll touch on shortly.

The HTML form controls that bind to these reactive form elements looks like this:


<-- sandbox/chapter-reactive-form/chapter-reactive-form.component.html -->
<form class="form-horizontal well well-sm" *ngIf="chapter" [formGroup]="form">
  ...
  <input id="name" type="text" class="form-control" formControlName="name">
  ...
  <select id="guild" class="form-control" formControlName="guild">
    <option [selected]="form.controls.guild.value == null" value="">-- Select --</option>
    <option *ngFor="let g of guildArray" [selected]="g.id == guild" [value]="g.id">{{g.name}}</option>
  </select>
  ...
  <input type="radio" formControlName="headChapter" name="headChapter" value="true" [checked]="form.controls.headChapter.value === true"> Yes   
  <input type="radio" formControlName="headChapter" name="headChapter" value="false" [checked]="form.controls.headChapter.value === false"> No
  ...
  <ul formArrayName="defenses"> <!-- Must set the formArrayName -->
    <li *ngFor="let def of form.controls.defenses.controls; let i = index">
      <input type="checkbox" formControlName="{{i}}" > {{defenseArray[i].label}}
    </li>
  </ul>

A few things worth pointing out:

  • The "--Select--" option for the guild drop-down was something I added so that text was displayed in the select box when the current value was null. The control would work fine without it.
  • Setting the "checked" attribute on the radio buttons based on the current headChapter form control state was necessary in order to show the initial value.
  • When a form control belongs to either a sub-FormGroup or a FormArray (as in this case), you need to use the formGroupName or formArrayName as an attribute in an HTML element that encloses the HTML elements bound to the FormControls within the FormGroup or FormArray.

As mentioned earlier, a validator function returns an object literal with context information about the validation problem when validation fails.  Those object literals need to be translated into appropriate error messages to display to the user.  So in a similar fashion to what I did with my template-driven form, I had to provide some translations in my component as well as a collection of arrays to hold the translated error messages:


//sandbox/chapter-reactive-form/chapter-reactive-form.component.ts
  ...
  errMsgs: any = {
    name: [],
    guild: [],
    headChapter: [],
    defenses: []
  };

  translations: any = {
    name: {
      required: 'The name is required.',
      minlength: 'The name must be at least 4 characters long.',
      pattern: 'The name can only contain letters.'
    },
    guild: {
      required: 'Please select a guild.'
    },
    headChapter: {
      required: 'Please select either Yes or No.'
    },
    defenses: {
      noDefenses: 'The chapter must implement at least one defensive measure.'
    }
  };

Note how each translation block consists of the name of the form control and an object literal whose properties names match up with the object literal keys returned by the validator functions (including the one returned by my custom hasDefenses() function).

The checkFormValidity() function (executed when the form emits the event indicating a form value has changed) performs the work of examining the current validation errors generated by the reactive form controls and creating the proper user-appropriate error messages:


/sandbox/chapter-reactive-form/chapter-reactive-form.component.ts
  ...
  checkFormValidity( data?: any ){
    for( let k in this.errMsgs ) {
      this.errMsgs[k] = [];
      if( this.form.controls[k].errors && this.form.controls[k].dirty ) {
        for( let e in this.form.controls[k].errors ) {
          if( this.translations[k][e] ) {
            this.errMsgs[k].push( this.translations[k][e] );
          }
        }
      }
    }
  }

Note that the validation error translation only occurs when the invalid form control is in a "dirty" state:  like template-driven form HTML elements, each form control has status values denoting if the form control is pristine or dirty, valid or invalid, and untouched or touched.  Preventing the users from seeing any validation errors when the control is pristine is desirable when you have a form that may initially be empty:  you don't want to display an error on a required field before the user has entered any data.  However, there is a drawback:  if you do change a form value programmatically, the change will trigger a validation check on the form control but it won't change the control state to dirty.  The workaround for that is to manually mark the field as dirty prior to changing the value, as demonstrated in this method:


changeName() {
  this.form.controls['name'].markAsDirty();
  this.form.controls['name'].setValue( '999' ); //invalid based on the [a-zA-Z]+ pattern validator
}

Flipping back to the HTML template, the user-appropriate error messages are displayed under the form controls like so:


<div *ngIf="errMsgs.name.length" class="alert alert-danger">
  <ul>
    <li *ngFor="let error of errMsgs.name">
      {{error}}
    </li>
  </ul>
</div>

Another benefit to using the reactive form classes is that the FormGroup class comes with a reset() method that not only blanks/nulls out the targeted form control values, it also resets the form controls back to a pristine and untouched state.

The final piece of the puzzle was to write a submit method that would copy the form control values back to the Chapter object (I also coded the submit button to be disabled whenever the FormGroup representing the form was flagged as invalid):


  submitForm() {
    this.checkFormValidity()
    if( this.form.valid ) {
      this.chapter.name = this.form.value.name; //value is a key/value map
      this.chapter.guildId = +this.form.value.guild; //need this translated to number, hence +
      this.chapter.headChapter = this.form.value.headChapter === "true";
      this.chapter.defenses = [];
      for( let db in this.defenseBoxArray.controls ) {
        if( this.defenseBoxArray.controls[ db ].value == true ) {
          this.chapter.defenses.push( this.defenseArray[ db ].id )
        }
      }
    }
  }

I then added interpolations to the template that would display the state and raw error values of the form controls as well as the current Chapter model values so I could watch everything in action when using the form:

Some final notes:

  • You may notice in the animated GIF of the form that the reset action did not clear the radio buttons. That's due to the fact that I'm still using RC5: that bug was fixed in the official release of Angular 2.

  • Although you can supply a validator function as an argument when instantiating a new FormGroup, for some reason the FormBuilder syntax for creating a FormGroup does not allow you to provide a validator. So if you need to add validation to a FormGroup object, instantiate the object ahead of time and then reference it in the FormBuilder construction (just like I did with my FormArray).