Angular 4 Forms

Prerequisuites

  • Install Node.js.
  • Install angular-cli: npm i -g @angular/cli
  • Generate project: ng new angular-forms

Regular HTML form

This is how regular html form looks like:

<form>
  <label>Name
    <input type="text" placeholder="Name" name="name">
  </label>
  <input type="submit">
</form>

We will create same form with Angular using two approaches:

  • template-driven - form including validations is described in HTML template and Angular generates data model from it automatically or allows to bind it to existing model
  • reactive - form model is described in TypeScript source code and it need to be bound to HTML-form

Template-driven form

We start with Angular template-driven form:

Inside app-module.ts import FormsModule:

import { FormsModule} from '@angular/forms';

Run command to generate component: ng g c form

Add form to form-component.html:

<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
  <label>Name
    <input type="text" placeholder="Name" name="name" ngModel>
  </label>
  <input type="submit">
</form>

Add handler to form-component.ts:

  onSubmit(form: any): void {
    console.log('submitted ', form);
  }

Reactive form

Inside app-module.ts import ReactiveFormsModule:

import { ReactiveFormsModule} from '@angular/forms';

Generate component: ng g c form-reactive

Inside form-reactive.component.ts:

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

Create template form-reactive.component.html:

<h1>Reactive form</h1>
<form [formGroup]="reactiveForm"
      (ngSubmit)="onSubmit(reactiveForm.value)">
  <label>Name
    <input type="text"
          placeholder="Name"
          [formControl]="reactiveForm.controls['name']">
    <input type="submit">
  </label>
</form>

Add code form-reactive.component.ts:

import { Component, OnInit } from '@angular/core';

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

@Component({
  selector: 'app-form-reactive',
  templateUrl: './form-reactive.component.html',
  styleUrls: ['./form-reactive.component.css']
})

export class FormReactiveComponent implements OnInit {

  reactiveForm: FormGroup;

  constructor(fb: FormBuilder) {
    this.reactiveForm = fb.group({
      'name': ['John']
    });
  }

  ngOnInit() { }

  onSubmit(form: any): void {
    console.log(form);
  }

}

Compare

Templates: Template-driven vs. Reactive

Classes: Template-driven vs. Reactive

Adding validators to template-driven form

Let’s make input required, we use attribute required in HTML template:

 <input type="text" name="name" required  ngModel>

Then we can show error if form is not valid:

<div *ngIf="!f.valid">Form is not valid!</div>

Or we can show error only if one field is not valid:

<div *ngIf="!f.controls.name.valid">Field "name" is not valid!</div>

Adding validators to reactive form

At import section of form-reactive.component.ts import Validators:

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

Declare control as field in the class:

name: AbstractControl;

Assign a validator to the FormControl object and value to field:

constructor(fb: FormBuilder) {
    this.reactiveForm = fb.group({
      'name': ['', Validators.required]
    });
    this.name = this.reactiveForm.controls['name'];
  }

We can check status of form validity with reactiveForm.valid and show message:

<div *ngIf="!reactiveForm.valid">Form is not valid!</div>

Also we can check status of specific field with name.valid:

<div *ngIf="!name.valid">Field "name" is not valid!</div>

Or check for some specific error:

<div *ngIf="name.hasError('required')">Field "name" is required!</div>

We may also do field coloring: form-reactive.component.css:

.error {
  border-width: 1px;
  border-color: red;
  border-style: solid;
}

form-reactive.component.html:

 <input type="text"
          placeholder="Name"
          [formControl]="name"
          [class.error]="name.touched && !name.valid">

Custom Validator

Create validation function:

  nameValidator(control: AbstractControl ): ValidationErrors | null {
    if (!control.value.match(/^[A-Z]+/)) {
      return {'nameShouldStartWithCapitalLetter': true};
    }
  }

Assign it as second validator:

this.reactiveForm = fb.group({
  'name': ['', Validators.compose([
                  Validators.required, this.nameValidator
          ])]
});

Show specific error in template:

<div *ngIf="name.hasError('nameShouldStartWithCapitalLetter')">Name should start with capital letter!</div>

Watching for changes

We can subscribe to changes for the whole form or specific fields only. Let’s extend our constructor:

constructor(fb: FormBuilder) {
this.reactiveForm = fb.group({
  'name': ['', Validators.compose([
                  Validators.required, this.nameValidator
          ])]
});
this.name = this.reactiveForm.controls['name'];

this.name.valueChanges.subscribe(
  (value: string) => {
  console.log('sku changed to:', value);
  }
);

this.name.valueChanges.subscribe(
  (form: any) => {
  console.log('form changed to:', form);
  }
);
}

Using two-way data binding

Let’s add another field e-mail:

email: string;

Add it to declaration of a form in constructor:

constructor(fb: FormBuilder) {
    this.reactiveForm = fb.group({
      'name': ['', Validators.compose([
                      Validators.required, this.nameValidator
              ])],
      'email': ['']
    });
...

Now we can use email in our html template:

{{ email }}}