Intro to Angular 4 Forms: Template-Driven vs. Reactive
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:
Classes:
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 }}}