Using Angular 2’s Model-Driven Forms with FormGroup and FormControl

Using Angular 2’s Model-Driven Forms with FormGroup and FormControl

  • 2016-07-26
  • 1996

Since Angular RC.2, Angular has introduced new forms modules and deprecated the existing one. With the new forms module, we can build model-driven forms with even more intuitive syntax.

There are two ways to build forms in Angular 2, namely template-driven and model-driven.

In this article, we will learn about building model-driven form with validation using the latest forms module, then we will talk about what are the advantages / disadvantages of using model driven form as compared to template-driven form. Please refer to How to Build Template-driven Forms in Angular 2 if you would like to learn about template-driven forms.

Introduction

We will build a form to capture user information based on this interface.

export interface User {
    name: string; // required with minimum 5 chracters
    address?: {
        street?: string; // required
        postcode?: string;
    }
}

Here is how the UI will look:

Angular 2 Model-Driven Forms

Requirements

  1. Show error message only when:
    • the field is invalid and it’s dirty (the field is touched/edited), or
    • the field is invalid and the form is submitted
  2. Listen and display form changes:
    • when any form values change
    • when form status (form validity) change
  3. Update the initial name field value to ‘Johnwithout trigger form changes.

App Setup

As of RC.2 - RC.4, deprecated forms is enabled by default.

Here’s our file structure:

|- app/
    |- app.component.html
    |- app.component.ts
    |- main.ts
    |- user.interface.ts
|- index.html
|- styles.css
|- tsconfig.json

In order to use new forms module, we need to npm install @angular/forms npm package and enable it during application bootstrap.

$ npm install @angular/forms --save

Here’s the bootstrapping for our application in main.ts:

// main.ts
import { bootstrap } from [email protected]/platform-browser-dynamic';
import { AppComponent } from './app/';
import { disableDeprecatedForms, provideForms } from [email protected]/forms';

bootstrap(AppComponent, [
  disableDeprecatedForms(), // disable deprecated forms
  provideForms(), // enable new forms module
]);

The App Component

Let’s move on to create our app component.

// app.component.ts

import { Component, OnInit } from [email protected]/core';
import { Validators } from [email protected]/common';
import { REACTIVE_FORM_DIRECTIVES, FormGroup, FormControl, FormBuilder } from [email protected]/forms';

import { User } from './user.interface';

@Component({
    moduleId: module.id,
    selector: 'my-app',
    templateUrl: 'app.component.html',
    directives: [REACTIVE_FORM_DIRECTIVES], // required for model-driven form
})
export class AppComponent implements OnInit {
    public myForm: FormGroup; // our model driven form
    public submitted: boolean; // keep track on whether form is submitted
    public events: any[] = []; // use later to display form changes

    constructor(private _fb: FormBuilder) { } // form builder simplify form initialization

    ngOnInit() {
        // we will initialize our form model here
    }

    save(model: User, isValid: boolean) {
        this.submitted = true; // set form submit to true

        // check if model is valid
        // if valid, call API to save customer
        console.log(model, isValid);
    }
}

Notes

  1. In order to use model driven form, we have to inject REACTIVE_FORM_DIRECTIVES to component directives.
  2. myForm will be our model driven form. It implements FormGroup interface.
  3. FormBuilder is not a mandatory to building model driven form, but it simplify the syntax, we’ll cover this later.

The HTML View

This is how our HTML view will look like.

<!-- app.component.html -->

<form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm.value, myForm.valid)">

    <!-- We'll add our form controls here -->

    <button type="submit">Submit</button>

</form>

We make sure we bind formGroup to our myForm property in app.component.ts file.

We’ll handle the form submit (ngSubmit) event in save() function that we defined in our app.component.ts file.

Implementation

All set! Let’s implement our model-driven form.

Initialize the Form Model

There are two ways to initialize our form model using model-driven forms in Angular 2.

Here is the long way to define a form:

// app.component.ts

ngOnInit() {

    // the long way
    this.myForm = new FormGroup({
        name: new FormControl('', [<any>Validators.required, <any>Validators.minLength(5)]),
        address: new FormGroup({
            street: new FormControl('', <any>Validators.required),
            postcode: new FormControl('8000')
        })
    });

}

And here’s the short way (using the form builder):

// app.component.ts

ngOnInit() {

    // the short way
    this.myForm = this._fb.group({
            name: ['', [<any>Validators.required, <any>Validators.minLength(5)]],
            address: this._fb.group({
                street: ['', <any>Validators.required],
                postcode: ['']
            })
        });

}

Both of these options will achieve the same outcome. The latter just has a simpler syntax.

A form is a type of FormGroup. A FormGroup can contain one FormGroup or FormControl. In our case, myForm is a FormGroup. It contains:

  • A name FormControl
  • An address FormGroup

The address FormGroup contains 2 form controls:

  • street
  • postcode

We can define a validator for both FormGroup and FormControl. Both accept either a single validator or array of validators.

Angular 2 comes with a few default validators and we can build our custom validator too. In our case, name has two validators:

  • required
  • minLength

Street has only one required validator.

Adding Name Control to the View

Let’s add the user’s name control to our view.

<!-- app.component.html -->
...

<!-- We'll add our form controls here -->
<div>
    <label>Name</label>
    <input type="text" formControlName="name">
    <small [hidden]="myForm.controls.name.valid || (myForm.controls.name.pristine && !submitted)">
        Name is required (minimum 5 characters).
    </small>
</div>

...
  • We haved assigned name to formControlName
  • For validation, since formControl has no export value, we need to read the errors information from our form model.
  • In our case, to check if name field is valid, or if it’s pristine, we’ll need to get the value from myForm controls, e.g. myForm.controls.name.valid. Very long syntax, huh.

Add an address form group to the view

Next we’ll add our address form group to the view.

<!-- app.component.html -->
....
<div formGroupName="address">
    <label>Address</label>
    <input type="text" formControlName="street">
    <small [hidden]="myForm.controls.address.controls.street.valid || (myForm.controls.address.controls.street.pristine && !submitted)">
        street required
    </small>
</div>
<div formGroupName="address">
    <label>Postcode</label>
    <input type="text" formControlName="postcode">
</div>
...

We have assigned the group name address to formGroupName. Please note that formGroupName can be used multiple times in the same form. In many examples, you’ll see people do this:

<!-- app.component.html -->
...
    <div formGroupName="address">
        <input formControlName="street">
        <input formControlName="postcode">
    </div>
...

This gives us the same results as above:

<!-- app.component.html -->
...
    <div formGroupName="address">
        <input formControlName="street">
    </div>
    <div formGroupName="address">
        <input formControlName="postcode">
    </div>
...

This is the same process as the previous section to bind form control.

Now the syntax gets even longer to retrieve control information. Oh my, myForm.controls.address.controls.street.valid.

How do we update the form value?

Now, imagine we need to assign default user’s name John to the field. How can we do that?

The easiest way is if John is static value:

// app.component.ts
...
this.myForm = this._fb.group({
    name: ['John', [ <any>Validators.required,   
    <any>Validators.minLength(5)]]
});
...

What if John is not a static value? We only get the value from API call after we initialize the form model. We can do this:-

// app.component.ts
...
(<FormControl>this.myForm.controls['name'])
    .updateValue('John', { onlySelf: true });
...

The form control exposes a function call updateValue which we can call to update our form control value.

updateValue accept optional parameter. In our case, we pass in { onlySelf: true }, mean this change will only affect the validation of this control and not its parent component. (read angular documentation for more details).

By default this.myForm.controls[‘name’] is of type AbstractControl. AbstractControl is the base class of FormGroup and FormControl. Therefore, we need to cast it to FormControl in order to utilize control specific function.

How about updating the whole form model?

As of RC.4, Angular forms version 0.2.0, there is no way for you to update the form model. However, as mentioned in the Angular form proposal document, it’s coming soon!

We can do something like this later on:

const user = {
    name: 'John',
    address: {
        street: 'High street',
        postcode: '94043'
    }
};
this.myForm.updateValue(user);

But not now. :(

Advantages of Model-Driven Forms

Now that we’ve build our model driven form. What are the advantages of using it over template driven form?

Unit testable

Since we have the form model defined in our code, we can unit test it. We won’t discuss detail about testing in this article.

Listen to form and controls changes

With reactive forms, we can listen to form or control changes easily. Each form group or form control expose a few events which we can subscribe to (e.g. statusChanges, valuesChanges, etc).

Let say we want to do something every time when any form values changed. We can do this:-

subcribeToFormChanges() {
    // initialize stream
    const myFormValueChanges$ = this.myForm.valueChanges;

    // subscribe to the stream 
    myFormValueChanges$.subscribe(x => this.events
        .push({ event: ‘STATUS CHANGED’, object: x }));
}

Then call this function in our ngOnInit().

ngOnInit() {
    // ...omit for clarity...
    // subscribe to form changes 
    this.subcribeToFormChanges();
}

Then display all value changes event in our view.

<!-- app.component.html -->
...
Form changes:
<div *ngFor="let event of events">
    <pre> {{ event | json }} </pre>
</div>
...

We can imagine more advanced use cases such as changing form validation rules dynamically depends on user selection, etc. Model driven form makes this simpler.

Model-Driven or Template-Driven?

It depends. If you are not doing unit testing (of course you should!), or you have simple form, go ahead with template-driven forms.

If you are not doing unit testing or you have simple form, go ahead with template-driven forms.

If you have advanced use cases, then consider model driven form.

Something good about template-driven forms as compared to model driven forms, imho:

  1. Template driven form has form.submitted flag in the exported ngForm, while model driven form don’t have that.
  2. Reading form control property in model driven form is not syntax friendly in the view. In our example, the syntax of reading address validity is myForm.controls.address.controls.street.valid while in template driven form we have exported ngModel, the syntax can be shorten to just street.valid. While understand that no exported member is done on purpose in the new design, the long syntax is still eye hurting…
  3. More familar syntax if you are coming from Angular 1.

Summary

That’s it! Now that you know how to build model-driven form, how about complex and nested model-driven forms? Says, we allow the user to enter multiple addresses now, how can we handle form array and validation? You might be interest in How to Build Nested Model-driven Forms in Angular 2.

Happy coding.

Suggest for you:


Angular 2 and NodeJS - The Practical Guide to MEAN Stack 2.0

Learn Angular 2 Development By Building 10 Apps

Angular 2 - The Complete Guide (Updated to RC4!)

Angular 2 with TypeScript for Beginners: The Pragmatic Guide