Saturday, January 5, 2019

Angular 2 form fundamentals: reactive forms

Angular 2 form fundamentals: reactive forms


High-level terminology

Before we begin, let’s clarify what “reactive” forms mean from a high level.

Reactive

When we talk about “reactive” forms (also known as model-driven), we’ll be avoiding directives such as ngModel, required and friends. The idea is that instead of declaring that we want Angular to power things for us, we can actually use the underlying APIs to do them for us. In a sense, instead of binding Object models to directives like template-driven forms, we in fact boot up our own instances inside a component class and construct our own JavaScript models. This has much more power and is extremely productive to work with as it allows us to write expressive code, that is very testable and keeps all logic in the same place, instead of scattering it around different form templates.

Template-driven forms

If you’re yet to dive into “template-driven” forms, check out my previous post on it.

Form base and interface

The base form structure that we’ll be using to implement our reactive form:
<form novalidate>
  <label>
    <span>Full name</span>
    <input
      type="text"
      name="name"
      placeholder="Your full name">
  </label>
  <div>
    <label>
      <span>Email address</span>
      <input
        type="email"
        name="email"
        placeholder="Your email address">
    </label>
    <label>
      <span>Confirm address</span>
      <input
        type="email"
        name="confirm"
        placeholder="Confirm your email address">
    </label>
  </div>
  <button type="submit">Sign up</button>
</form>
We have three inputs, the first, the user’s name, followed by a grouped set of inputs that take the user’s email address.
Things we’ll implement:
  • Bind to the user’s name, email, and confirm inputs
  • Required validation on all inputs
  • Show required validation errors
  • Disabling submit until valid
  • Submit function
Secondly, we’ll be implementing this interface:
// signup.interface.ts
export interface User {
  name: string;
  account: {
    email: string;
    confirm: string;
  }
}

ngModule and reactive forms

Before we even dive into reactive forms, we need to tell our @NgModule to use the ReactiveFormsModule from @angular/forms:
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ...,
    ReactiveFormsModule
  ],
  declarations: [...],
  bootstrap: [...]
})
export class AppModule {}
You will obviously need to wire up all your other dependencies in the correct @NgModule definitions.
Tip: use ReactiveFormsModule for reactive forms, and FormsModule for template-driven forms.

Reactive approach

Let’s begin with a base SignupFormComponent and add our above template:
// signup-form.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'signup-form',
  template: `
    <form novalidate>...</form>
  `
})
export class SignupFormComponent {
  constructor() {}
}
So, this is a typical component base that we need to get going. So what now? Well, to begin with, we don’t need to actually create any initial “data”, however, we do need to start understanding FormControl, FormGroup, and finally move onto the amazing FormBuilder.

FormControl and FormGroup

Before digging into these APIs, I would strongly recommend checking out my previous article on template-driven forms to gain a better understanding of what’s happening.
Let’s define what FormControl and FormGroup are:
  • FormControl is a class that powers an individual form control, tracks the value and validation status, whilst offering a wide set of public API methods.
Basic example:
ngOnInit() {
  this.myControl = new FormControl('Todd Motto');
}
  • FormGroup is a group of FormControl instances, also keeps track of the value and validation status for the said group, also offers public APIs.
Basic example:
ngOnInit() {
  this.myGroup = new FormGroup({
    name: new FormControl('Todd Motto'),
    location: new FormControl('England, UK')
  });
}
Right, so we have an example of invoking new instances of FormControl and FormGroup, now how do we use them? It’s actually much easier than you’d think. Let’s assume we’ll bind our FormGroup to a fresh code example before we continue with our signup form, so hopefully things click and you can follow easier:
<form novalidate [formGroup]="myGroup">
  Name: <input type="text" formControlName="name">
  Location: <input type="text" formControlName="location">
</form>
Note: you’ll notice ngModel and name="" attributes have been toasted, this is good thing as it makes our markup less declarative (which can become complex, quickly, with forms)
That’s it! On the form, we must declare [formGroup] as a binding, and formControlName as a directive with the corresponding Object key name. This is what we have:
FormGroup -> 'myGroup'
    FormControl -> 'name'
    FormControl -> 'location'

Implementing our FormGroup model

So now we’ve learned the basis of FormGroup and FormControl, we can think about implementing our own now. But first, what does our interface say?
// signup.interface.ts
export interface User {
  name: string;
  account: {
    email: string;
    confirm: string;
  }
}
So, we’ll need to implement a similar structure with JavaScript Objects using this composition:
FormGroup -> 'user'
    FormControl -> 'name'
    FormGroup -> 'account'
        FormControl -> 'email'
        FormControl -> 'confirm'
Yes, we can create nested FormGroup collections! Let’s make that come alive, but with no initial data:
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({...})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  ngOnInit() {
    this.user = new FormGroup({
      name: new FormControl(''),
      account: new FormGroup({
        email: new FormControl(''),
        confirm: new FormControl('')
      })
    });
  }
}
If we did want to set initial data, we can do so as per the above examples whereby we pre-populate particular strings with information, which typically are data-driven from a backend API.

Binding our FormGroup model

Now we’ve instantiated the FormGroup model, it’s obviously time to bind it to the DOM. Using what we’ve learned before, let’s go ahead:
<form novalidate [formGroup]="user">
  <label>
    <span>Full name</span>
    <input
      type="text"
      placeholder="Your full name"
      formControlName="name">
  </label>
  <div formGroupName="account">
    <label>
      <span>Email address</span>
      <input
        type="email"
        placeholder="Your email address"
        formControlName="email">
    </label>
    <label>
      <span>Confirm address</span>
      <input
        type="email"
        placeholder="Confirm your email address"
        formControlName="confirm">
    </label>
  </div>
  <button type="submit">Sign up</button>
</form>
Now our FormGroup and FormControl matches with the DOM structure:
// JavaScript APIs
FormGroup -> 'user'
    FormControl -> 'name'
    FormGroup -> 'account'
        FormControl -> 'email'
        FormControl -> 'confirm'

// DOM bindings
formGroup -> 'user'
    formControlName -> 'name'
    formGroupName -> 'account'
        formControlName -> 'email'
        formControlName -> 'confirm'
Unlike template-driven forms, where we would do something like #f="ngForm", and print f.value in the DOM to check our form out, we do the opposite with reactive forms, as the [formGroup] is a directive that we bind to, passing the public user Object in:
// { name: '', account: { email: '', confirm: '' } }
{{ user.value | json }}

Reactive submit

This is actually the exact same as the template-driven approach, however we can optionally reference the form internally to the component, instead of passing it in as a value. First, the ngSubmit value-passing:
<form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
  ...
</form>
Notice how we just passed user into the onSubmit()? This allows us to pull down various pieces of information from our respective method on our component class:
export class SignupFormComponent {
  user: FormGroup;
  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}
Here we’re using Object destructuring to fetch the value and valid properties from the user reference we pass into onSubmit. The value is the same reference as printing user.value out in the DOM. That’s literally it, you’re free to pass values to your backend API.
Now, for the more internal approach. Because this.user is technically our model, we can simply reference the model onSubmit internally, and not pass user through as a function argument:
export class SignupFormComponent {
  user: FormGroup;
  onSubmit() {
    console.log(this.user.value, this.user.valid);
  }
}

Reactive error validation

So far, we’ve implemented zero validation! Oh my. Let’s fix this. To add validation, we actually need to import the lovely Validators from @angular/forms and pass them in as a second argument to our FormControl instances:
ngOnInit() {
  this.user = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(2)]),
    account: new FormGroup({
      email: new FormControl('', Validators.required),
      confirm: new FormControl('', Validators.required)
    })
  });
}
Rule: need multiple Validators per FormControl? Use an array to contain them.
This is now a replacement for adding <input required> to the DOM, which means we never have to touch it. Internally, when using required directives in template-driven forms, Angular will actually create this stuff under-the-hood for us, so that’s the main difference between the two implementations.
However, we are going to create [disabled] binding just like in the template-driven approach to disable the submit when the form is invalid:
<form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
  ...
  <button type="submit" [disabled]="user.invalid">Sign up</button>
</form>
All ready to go, now when we actually have validation errors, we need to now show them. When it comes to referencing the controls powering the errors, we must use the .controls property on the Object. Let’s say we want to show if there are any errors on the name property of our form:
<form novalidate [formGroup]="user">
  {{ user.controls.name?.errors | json }}
</form>
Tip: ?.prop is called the “Safe navigation operator”
We also have a .get() method that will lookup that control (I much prefer this as it’s a nicer API and avoids ?.errors):
<form novalidate [formGroup]="user">
  {{ user.get('name').errors | json }}
</form>
So, onto implementing the validation, we need to add the following to the correct portions of the form:
<!-- name -->
<div
  class="error"
  *ngIf="user.get('name').hasError('required') && user.get('name').touched">
  Name is required
</div>
<div
  class="error"
  *ngIf="user.get('name').hasError('minlength') && user.get('name').touched">
  Minimum of 2 characters
</div>

<!-- account -->
<div
  class="error"
  *ngIf="user.get('account').get('email').hasError('required') && user.get('account').get('email').touched">
  Email is required
</div>
<div
  class="error"
  *ngIf="user.get('account').get('confirm').hasError('required') && user.get('account').get('confirm').touched">
  Confirming email is required
</div>
Tip: The touched property becomes true once the user has blurred the input, which may be a relevant time to show the error if they’ve not filled anything out

Code so far

This is what we’ve achieved up until now:
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { User } from './signup.interface';

@Component({
  selector: 'signup-form',
  template: `
    <form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
      <label>
        <span>Full name</span>
        <input type="text" placeholder="Your full name" formControlName="name">
      </label>
      <div class="error" *ngIf="user.get('name').hasError('required') && user.get('name').touched">
        Name is required
      </div>
      <div class="error" *ngIf="user.get('name').hasError('minlength') && user.get('name').touched">
        Minimum of 2 characters
      </div>
      <div formGroupName="account">
        <label>
          <span>Email address</span>
          <input type="email" placeholder="Your email address" formControlName="email">
        </label>
        <div
          class="error"
          *ngIf="user.get('account').get('email').hasError('required') && user.get('account').get('email').touched">
          Email is required
        </div>
        <label>
          <span>Confirm address</span>
          <input type="email" placeholder="Confirm your email address" formControlName="confirm">
        </label>
        <div
          class="error"
          *ngIf="user.get('account').get('confirm').hasError('required') && user.get('account').get('confirm').touched">
          Confirming email is required
        </div>
      </div>
      <button type="submit" [disabled]="user.invalid">Sign up</button>
    </form>
  `
})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor() {}
  ngOnInit() {
    this.user = new FormGroup({
      name: new FormControl('', [Validators.required, Validators.minLength(2)]),
      account: new FormGroup({
        email: new FormControl('', Validators.required),
        confirm: new FormControl('', Validators.required)
      })
    });
  }
  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}

Simplifying with FormBuilder

This is where things get even smoother! Instead of using FormGroup and FormControl directly, we can use a magical API underneath that does it all for us. Meet FormBuilder!
First up, we’ll need to change our imports from this:
import { FormControl, FormGroup, Validators } from '@angular/forms';

export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor() {}
  ...
}
To this (with additional constructor injection to make this.fb available as the FormBuilder):
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor(private fb: FormBuilder) {}
  ...
}
This is because user: FormGroup; on our component class is of type FormGroup. So, what is FormBuilder? It’s essentially syntax sugar that creates FormGroup, FormControl and FormArray instances for us (we’ll cover FormArray in another article). It’s just simple sugar, but now you know what it’s for.
Let’s refactor our code to use FormBuilder:
// before
ngOnInit() {
  this.user = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(2)]),
    account: new FormGroup({
      email: new FormControl('', Validators.required),
      confirm: new FormControl('', Validators.required)
    })
  });
}

// after
ngOnInit() {
  this.user = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    account: this.fb.group({
      email: ['', Validators.required],
      confirm: ['', Validators.required]
    })
  });
}
The refactoring is self-explanatory, but let’s roll over it quickly.
Instead of using new FormGroup() for example, we’re injecting FormBuilder as fb, and creating a new this.fb.group(). The structure of these are identical to creating the controls and groups by themselves, it’s just syntax sugar. Which leaves us with a component class that looks like this:
@Component({...})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor(private fb: FormBuilder) {}
  ngOnInit() {
    this.user = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2)]],
      account: this.fb.group({
        email: ['', Validators.required],
        confirm: ['', Validators.required]
      })
    });
  }
  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}

No comments:

Post a Comment

48 answers on StackOverflow to the most popular Angular questions I gathered the most common questions and answers from Stac...