Intro to Angular's Reactive Forms

Published 12/12/2018 01:27 PM   |    Updated 01/08/2019 11:27 AM
 
Forms are an inevitable part of any front-end development project — they’re the basis of how applications collect data. Angular attempts to simplify the form creation process by providing two approaches to tackling form development: template-driven forms and reactive forms.
 
What’s the difference? Template-driven forms are managed in the HTML template, whereas reactive forms are managed in the component.
 
When to use one over the other? The template-driven option is good for basic forms with a fixed number of inputs and validation of the entire form, instead of each individual input. Reactive-driven forms are best for complex forms with a dynamic number of inputs, validation of each input and projects that require unit testing.
 
 
As the title of this article indicates, we’ll be looking at reactive forms. Why reactive forms? I’ve experienced the power of reactive forms on client projects and how relatively simple it is to develop complex forms.
 
For you to get the most out of this article, I recommend you have some basic Angular 2+ knowledge.
 
During this step-by-step guide, the technologies we’ll use are TypeScript and Angular’s Command-Line Interface (CLI). (To use the CLI, you’ll need Node/NPM installed, which we’ll cover as we go along.) Although the example form we’re building doesn’t have submit functionality, it highlights the basics of Angular’s reactive forms, including:
 
  • Validation for required fields, proper email formatting
  • Showing/hiding validation messages
  • Dynamically adding/removing CSS classes based on validations
  • Disabling/enabling the Submit button
  • Resetting the form
 

Project setup with Angular CLI

 
To simplify the setup process, we’ll leverage Angular CLI. To install the CLI, if you don’t have it already, follow along with Angular’s documentation.
 
Paraphrasing the documentation:
 
  • Ensure you have Node 6.9+ and NPM 3+ installed. You can check by entering the following commands in your command prompt/terminal. If you find they need to be installed or updated, check out https://nodejs.org/en/.
    node-v
    npm-v
  • After installing/updating Node and NPM, we’ll need to install TypeScript and the Angular CLI:
    npm intall -g typescript @angular/cli
 

Creating and serving the project

 
In the command prompt, navigate to a directory where you’d like your project to live and create the project by entering:
 
ng new reactive-forms-app
 
The CLI will install all of the appropriate JavaScript packages for you. Once the packages have been installed, you need to run the project. Using the command prompt, change your directory to the reactive-forms-app and run the following command:
 
ng serve OR npm start
 
The application should be running now. Check it out on http://localhost:4200/, unless you’ve specified a different port. After you save the changes you make during development, the browser will refresh automatically.
 

Creating the component

 

Using Angular CLI to create the component

 
We’re ready to make the component; the CLI will create the template, component, CSS and test files for you. Inside the root of the project’s directory enter:
 
ng generate component reactive-form
 

Reference form modules and the new component

 
Because we’re using Angular forms, we must import the appropriate modules for our project in the app.module.ts file, located in the app directory. The modules we need are: FormsModule and ReactiveFormsModule from @angular/forms.
 
In the @NGModule decorator, we need to reference the modules we just imported in the file. We need to place them inside the imports array. We also need to reference the reactive-form component we just created. Open your favorite code editor and paste in this code:
 
app.module.ts.
 
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { ReactiveFormComponent } from './reactive-form/reactive-form.component';

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

Updating the template

 
After the CLI generates the component, we’ll plug in the HTML for the form in the reactive-form.comonent.html file:
 
reactive-form.component.html
 
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<form novalidate [formGroup]="bookForm">

  <h1>Reactive Form Example</h1>

  <div class="group">
    <label for="title">Book Title * </label>
    <input type="text"
      [class.error]="!bookTitle.valid && bookTitle.touched"
      [formControl]="bookTitle"
      id="title" 
      placeholder="Book Title" 
      required>
      <div *ngIf="bookTitle.touched">
        <p *ngIf="bookTitle.hasError('required')">Field is required.</p>
        <p *ngIf="bookTitle.hasError('maxlength')">
          Book title must be less than {{maxBookLength}} characters.</p>
      </div>
  </div>

  <div class="group">
    <label for="author-first-name">Author's First Name *</label>
    <input type="text"
      [formControl]="firstName"
      [class.error]="!firstName.valid && firstName.touched"
      id="author-first-name" 
      class="medium" 
      placeholder="Author First Name" 
      required>
      <p *ngIf="!firstName.valid && firstName.touched">Field is required.</p>
  </div>

  <div class="group">
    <label for="author-last-name">Author's Last Name *</label>
    <input type="text" 
      [formControl]="lastName"
      [class.error]="!lastName.valid && lastName.touched"
      id="author-last-name" 
      class="medium" 
      placeholder="Author Last Name" 
      required>
      <p *ngIf="!lastName.valid && lastName.touched">Field is required.</p>      
  </div>

  <div class="group">
    <label for="author-email-address">Author's Email</label>
    <input type="email" 
      [formControl]="emailAddress"
      id="author-email-address"
      placeholder="Author Email"
      [class.error]="!emailAddress.valid && emailAddress.touched" 
      required>
        <p *ngIf="emailAddress.touched&& emailAddress.hasError('invalidEmail')">Enter a valid email address.</p>
    </div>

  <div class="group">
    <label for="genre">Genre *</label>
    <input type="text"
      [class.error]="!genre.valid && genre.touched"
      [formControl]="genre" 
      id="genre" 
      placeholder="Genre" 
      required>
    <p *ngIf="!genre.valid && genre.touched">Field is required.</p>
  </div>

  <div class="checkbox">
    <label>
      <input type="checkbox" [formControl]="bookRead" value="true">Read</label>
  </div>
  <br>
  <br>
  <p><strong>*</strong> Indicates a required field.</p>
  <hr>
  <button type="button" [disabled]="!bookForm.valid">Submit</button>
  <button class="clear" (click)="initializeForm()" type="button">Clear</button>

</form>
 

Updating the component TypeScript file

 
We’ll talk about what’s going on in the other elements of the template, but for now, we’ll cover the crux of Angular reactive forms: the component. Below is the completed reactive-form.component.ts file.
 
reactive-form.component.ts
 
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators, AbstractControl, FormControl } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  templateUrl: './reactive-form.component.html'
})
export class ReactiveFormComponent implements OnInit {

  constructor(private fb: FormBuilder) { }

  bookForm: FormGroup;
  bookTitle: AbstractControl;
  firstName: AbstractControl;
  lastName: AbstractControl;
  emailAddress: AbstractControl;
  genre: AbstractControl;
  bookRead: AbstractControl;

  maxBookLength: number = 50;

  ngOnInit() {
    this.initializeForm();
  }

  initializeForm() {
    this.bookForm = this.fb.group({
      bookTitle: ['Anything in here will display inside the input', Validators.compose([
        Validators.required,
        Validators.maxLength(this.maxBookLength)
      ])],
      firstName: ['', Validators.required],
      lastName: ['', Validators.required],
      emailAddress: ['', Validators.compose([
        Validators.required,
        this.emailValidator
      ])],
      genre: ['', Validators.required],
      bookRead: [true]
    });
    this.bookTitle = this.bookForm.controls['bookTitle'];
    this.firstName = this.bookForm.controls['firstName'];
    this.lastName = this.bookForm.controls['lastName'];
    this.emailAddress = this.bookForm.controls['emailAddress'];
    this.genre = this.bookForm.controls['genre'];
    this.bookRead = this.bookForm.controls['bookRead'];
  }

  emailValidator(control: FormControl): { [s: string]: boolean } {
    if (!control.value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/)) {
        return { invalidEmail: true };
    }
  }

}
 
To tie our form into Angular, we need to import the appropriate classes into the component from @angular/forms. Specifically:
 
FormGroup, FormBuilder, Validators, AbstractControl and FormControl
 
After the import, we need to inject FormBuilder in the constructor. FormBuilder does the heavy lifting for us in reactive forms. I’m using Angular’s recommended method for injecting FormBuilder into our component, and I’m naming it “fb” to reference it throughout the component.
 
We’re initializing the form inside Angular’s ngOnInit lifecycle hook. I made initializeForm() a method so that when the user clicks the Clear button, the form is reset to its initial state.
 
Let’s cover what else is going on in the component.
 

FormGroup

 
Stores a reference to the form so we can use it throughout the component; this is the same name we used as the FormGroup attribute on the form element.
 

AbstractControl

 
Allows us to reference an input. This is best explained by an example: Without the AbstractControl, we’d have to reference our inputs in the validation messages as:
 
*ngIf=!bookForm.controls['bookTitle'].valid && bookForm.controls['bookTitle'].touched 
 
With the AbstractControl, we can concisely reference the input as:
 
*ngIf=!bookTitle.valid && bookTitle.touched
 

Validators

 
Give us access to Angular’s built-in validators and allow us to create our own and reference them in the template. For example:
 
<p *ngIf="bookTitle.hasError('required') && bookTitle.touched">Field is required.</p>
<p *ngIf="bookTitle.hasError('maxlength') && bookTitle.touched">
          Book title must be less than {{maxBookLength}} characters.
</p>
 
The parameter of this fb.group method accepts an object, where each FormControl is a key and the property of the key is an array. At the zero index, the string is the default value of the input. And at the first index, we’re referencing the validators. If you have multiple validations for an input, you must use the compose method on the validators class.
 
this.bookForm = this.fb.group({
bookTitle: ['Anything in here will display inside the input', Validators.compose([
        		Validators.required,
       		Validators.maxLength(this.maxBookLength)
      	])],
               	
 });
 

Toggling validation messages and CSS classes

 
We’ll only show validation error messages if there’s an error, and if the user “touched” the input. We covered this when we talked about AbstractControls, but we’ll dig a little deeper into validations.
 
Thanks to the FormBuilder, our inputs (FormControls) have access to a variety of methods and properties, specifically valid, touched and hasError():
 
  • FormControlName.valid checks the validity of the input.
  • FormControlName.touched has the user touched and navigated away from the input.
  • FormControlName.hasError() checks for different errors. This method accepts built-in and custom validators; we created our custom email validator called “invalidEmail.”
 
We can also toggle CSS classes based on the valid and touched properties available. For example, we’re only adding the error class to the input if the field is invalid and the field was touched:
 
[class.error]=!formControlName.valid && formControlName.touched
 

Disabling the Submit button and clearing the form

 
Just as our input fields have properties and methods, FormBuilder provides functionality to the form itself. Using these properties is how we disable the Submit button, until the fields are valid.
 
[disabled]=!bookForm.valid
 
To clear or reset the form, we call the initializeForm method on the click event.
 
(click)=initializeForm
 

Final step: Updating the app.component.html file

 
Open the app.component.html file and reference the element/component inside the app.component.html. In the file, you should have:
 
<app-reactive-form></app-reactive-form>
 
Once you’ve saved the changes to the app.component.html file, you should have a working form.
 

Tips when developing reactive forms

 
When you’re on your own developing an Angular reactive form, remember the following tips to avoid snafus and make development a little easier:
 
  • Ensure the values for the FormGroup and FormControl attributes in the HTML match the variable names in your TypeScript file.
  • Import the FormsModule and ReactiveFormsModule in your project.
  • Using the AbstractControl type will make it easier to reference your form inputs.
  • Only display validation error messages if the user touched the input and there’s an error; otherwise, your user will see validation error messages when the form loads.
 
Hopefully, you’ve discovered the value of using reactive forms and will incorporate them in your next Angular project.
 

Resource:

 
Angular Template-Driven Forms and Reactive Forms in a Nutshell
 
 

Explore more Digital Innovation insights →

 
This article originally appeared on July 2, 2018.

Is this answer helpful?