Reactive Forms in Angular

by Pascal Precht on Jun 22, 2016, last updated on Dec 18, 2016
10 minute read

Run your Machine Learning experiments in the browser

Learn more

Contents are based on Angular version >= 2.x unless explicitly stated differently.

Just a couple days ago, we’ve updated our article on Template-driven Forms in Angular, as the APIs have changed for the better in the second release candidate. We think it’s time to talk about model-driven and reactive forms on our blog. This article builds on top of the knowledge we’ve gained in our previous article, so you might want to take a look at it in case you haven’t yet.

The goal of reactive/model-driven forms

While template-driven forms in Angular give us a way to create sophisticated forms while writing as little JavaScript as possible, model-driven forms enable us to test our forms without being required to rely on end-to-end tests. That’s why model-driven forms are created imperatively, so they end up as actually properties on our components, which makes them easier to test.

In addition, it’s important to understand that when building reactive/model-driven forms, Angular doesn’t magically create the templates for us. So it’s not that we just create the form model and then all of a sudden we have DOM generated in our app that renders the form. Reactive forms are more like an addition to template-driven forms, although, depending on what we’re doing some things can be left out here and there (e.g. validators on DOM elements etc.).

FormGroup and FormControl

Let’s start off again with the same form we used in our previous article, a form to register a new user on a platform:

<form>
  <label>Firstname:</label>
  <input type="text">

  <label>Lastname:</label>
  <input type="text">

  <label>Street:</label>
  <input type="text">

  <label>Zip:</label>
  <input type="text">

  <label>City:</label>
  <input type="text">

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

Nothing special going on here. We simply ask for a firstname, a lastname and some address information. Now, to make this form model-driven, what we need to do is to create a form model that represents that DOM structure in our component. One way to do that is to use the low level APIs for FormGroup and FormControl.

FormGroup always represents a set of FormControls. In fact, a form is always a FormGroup. Let’s create the corresponding form model for our template:

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

@Component({
  selector: 'my-app',
  ...
})
export class AppComponent {

  registerForm = new FormGroup({
    firstname: new FormControl(),
    lastname: new FormControl(),
    address: new FormGroup({
      street: new FormControl(),
      zip: new FormControl(),
      city: new FormControl()
    })
  });
}

Uff, that looks quite wordy! We’ll fix that in a second but let’s first discuss what happened. We created a component property registerForm which represents the FormGroup, which is our form. For each field in the form, we’ve created a FormControl and what we can’t see here, is that a FormControl takes a string as first argument in case we want to prefill the form control with a default value.

Another nice thing to notice is that FormGroup’s can be nested. address is also a collection of form controls, even though it doesn’t show up in the DOM (yet). We’ll see in a second why that is.

Okay, now that we’ve created our first form model, let’s associate it to our template.

Binding forms using formGroup, formGroupName and formControlName

Currently, there’s nothing in our code that tells Angular that our form model is responsible for the form template. We need to associate the model to our form, and we can do that using the formGroup directive, which takes an expression that evaluates to a FormGroup instance.

In order to use that directive we need to import the ReactiveFormsModule into our application module:

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

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

Now we can go ahead and use formGroup to connect the model with the form template:

<form [formGroup]="registerForm">
  ...
</form>

Great, the next thing we need to do is to associate the form controls to the model as well, because there’s nothing that tells Angular “Hey these form controls here belong to your form control models!”.

This is where the formControlName directive comes into play. It’s pretty much the equivalent of an ngModel and name attribute combination in template-driven forms. Each form control gets a formControlName directive applied so we can register the controls on the outer form:

<form [formGroup]="registerForm">
  <label>Firstname:</label>
  <input type="text" formControlName="firstname">

  <label>Lastname:</label>
  <input type="text" formControlName="lastname">

  <label>Street:</label>
  <input type="text" formControlName="street">

  <label>Zip:</label>
  <input type="text" formControlName="zip">

  <label>City:</label>
  <input type="text" formControlName="city">

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

And last but not least, since address is created as a FormGroup as well, we can associate a group of form controls to that model using formGroupName. However, we need a surrounding element for that, otherwise there’s no place where we can apply that directive. Let’s surround the address fields with a <fieldset> element and apply formGroupName there.

<fieldset formGroupName="address">
  <label>Street:</label>
  <input type="text" formControlName="street">

  <label>Zip:</label>
  <input type="text" formControlName="zip">

  <label>City:</label>
  <input type="text" formControlName="city">
</fieldset>

It’s time to make our code a bit more readable and replace FormGroup and FormControl with FormBuilder.

FormBuilder

As mentioned earlier, the creation of our form model looks quite wordy as we have to call new FormGroup() and new FormControl several times to construct the model. Luckily, we’ve used rather low level APIs and we can use a higher level API that makes this task a bit more pleasant.

FormBuilder is like a factory that creates FormGroup’s and FormControl’s for us. All we need to do is to import it and us its .group() method like this:

import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'my-app',
  ...
})
export class AppComponent implements OnInit {

  registerForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.registerForm = this.formBuilder.group({
      firstname: '',
      lastname: '',
      address: this.formBuilder.group({
        street: '',
        zip: '',
        city: ''
      })
    });
  }
}

This looks way better! Let’s recap really quick what happened:

  • We imported FormBuilder
  • We injected it into AppComponent
  • We created the form model using FormBuilder.group() in ngOnInit() lifecycle
  • We haven’t done any changes on the template

Adding Validators

Now that we have the form model set up, we can start adding validators to our form controls. There are different ways to add them, we can either add then as directives to the template or to the FormControl instance in our model.

Let’s say we want to add a validators that makes sure that firstname and lastname is set. Angular comes with a Validators class that has some common validators built-in. We can import and apply them right away:

import { 
  FormBuilder,
  FormGroup,
  Validators
} from '@angular/forms';

...

ngOnInit() {
  this.registerForm = this.formBuilder.group({
    firstname: ['', Validators.required],
    lastname: ['', Validators.required],
    address: this.formBuilder.group({
      street: [],
      zip: [],
      city: []
    })
  });
}

A FormControl takes a value as first, a synchronous validator as second and an asynchronous validator as third parameter. We can also pass a collection of validators which causes Angular to compose them for us. And all we do here is applying a synchronous validator (Validators.required) to our form controls.

We can access the validity state of a form control like this:

<form [formGroup]="registerForm">
  <label>Firstname:</label>
  <input type="text" formControlName="firstname">
  <p *ngIf="registerForm.controls.firstname.errors">This field is required!</p>

  ...
</form>

We get a reference to the form control by traversing the registerForm instance. If you’re interested in learning how to build a custom validator, you might want to read our article on Custom Validators in Angular.

Forms with a single control

Sometimes, we don’t need a FormGroup, as our form might only consist of a single form control. Think of a search field that let’s you search for products in an e-commerce application. Technically, we don’t even need a <form> element for that.

Angular comes with a directive formControl which doesn’t have to be inside a formGroup. We can simply add it to a single form control and are ready to go:

<!-- no surrounding form -->
<input type="search" [formControl]="seachControl">

The cool thing about form controls in Angular is, that we can listen reactively for changes that are happening to that control. Every form controls exposes an Observable propery valuesChanges() that we can subscribe to. So in order to get notified about changes in the example above, all we have to do is:

@Component()
export class SearchComponent implements OnInit {

  searchControl = new FormControl();

  ngOnInit() {
    this.searchControl.valueChanges.subscribe(value => {
      // do something with value here
    });
  }
}

Hopefully this one gave you a better idea of how reactive/model-driven and template-driven forms relate to each other.

Need a private training?

Get customized training for your corporation

Request quote

Get updates on new articles and trainings.

Join over 1400 other developers who get our content first.

Author

Related Posts