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

Angular 2's latest forms module lets us build model-driven forms easily.

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

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

Here's our file structure:

|- app/
    |- app.component.html
    |- app.component.ts
    |- app.module.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 import the reactive forms module in application module.

$ npm install @angular/forms --save

Here's the module for our application app.module.ts:

// app.module.ts

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

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

@NgModule({
  imports:      [ BrowserModule, ReactiveFormsModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})

export class AppModule { }

The App Component

Let's move on to create our app component.

// app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';

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

@Component({
    moduleId: module.id,
    selector: 'my-app',
    templateUrl: 'app.component.html',
})
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. myForm will be our model driven form. It implements FormGroup interface.
  2. 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'])
    .setValue('John', { onlySelf: true });
...

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

setValue 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.

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?

It's possible! We can do something like this:

// app.component.ts
...
    const people = {
            name: 'Jane',
            address: {
                street: 'High street',
                postcode: '94043'
            }
        };

        (<FormGroup>this.myForm)
            .setValue(people, { onlySelf: true });
...

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.

Jecelyn Yeen

Coder. Diver. Board Game Lover.

Speak English, Mandarin, JavaScript, Typescript, C# and more.

Problem solver at @iflix.