docs(cb-form-validation): add template2 - a step between template and reactive

Raises questions about what really separates Forms from ReactiveForms
This commit is contained in:
Ward Bell 2016-08-28 17:07:13 -07:00
parent f971685a7c
commit 1212b5147f
15 changed files with 726 additions and 245 deletions

View File

@ -1,4 +1,5 @@
/// <reference path="../_protractor/e2e.d.ts" />
'use strict'; // necessary for node!
describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () {
beforeEach(function () {

View File

@ -3,8 +3,10 @@ import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `<hero-form-template></hero-form-template>
template: `<hero-form-template1></hero-form-template1>
<hr>
<hero-form-reactive></hero-form-reactive>`
<hero-form-template2></hero-form-template2>
<hr>
<hero-form-reactive3></hero-form-reactive3>`
})
export class AppComponent { }

View File

@ -1,16 +1,19 @@
<!-- #docregion -->
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form (Reactive)</h1>
<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">
<h1>Hero Form 3 (Reactive)</h1>
<!-- #docregion form-tag-->
<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">
<!-- #enddocregion form-tag-->
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label>
<input type="text" id="name" class="form-control"
formControlName="name"
[ngClass]="{'required': isRequired('name')}">
<div *ngIf="formError.name" class="alert alert-danger">
{{ formError.name }}
formControlName="name" required >
<div *ngIf="formErrors.name" class="alert alert-danger">
{{ formErrors.name }}
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
@ -18,28 +21,27 @@
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" id="alterEgo" class="form-control"
formControlName="alterEgo"
[ngClass]="{'required': isRequired('alterEgo')}" >
formControlName="alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select id="power" class="form-control"
formControlName="power"
[ngClass]="{'required': isRequired('power')}" >
formControlName="power" required >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="formError.power" class="alert alert-danger">
{{ formError.power }}
<div *ngIf="formErrors.power" class="alert alert-danger">
{{ formErrors.power }}
</div>
</div>
<button type="submit" class="btn btn-default"
[disabled]="!heroForm.valid">Submit</button>
<button type="button" class="btn btn-default"
(click)="newHero()">New Hero</button>
(click)="addHero()">New Hero</button>
</form>
</div>
<hero-submitted [hero]="model" [(submitted)]="submitted"></hero-submitted>
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
</div>

View File

@ -4,104 +4,112 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Hero } from '../shared/hero';
import { Hero } from '../shared/hero';
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
@Component({
moduleId: module.id,
selector: 'hero-form-reactive',
selector: 'hero-form-reactive3',
templateUrl: 'hero-form-reactive.component.html'
})
// #docregion class
export class HeroFormReactiveComponent implements OnInit {
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
model = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
hero = new Hero(18, 'Dr. WhatIsHisName', this.powers[0], 'Dr. What');
submitted = false;
// #docregion on-submit
onSubmit() {
this.submitted = true;
this.model = this.heroForm.value;
this.hero = this.heroForm.value;
}
// #enddocregion class
// #enddocregion on-submit
// #enddocregion
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
// #docregion new-hero
active = true;
// #docregion class
newHero() {
this.model = new Hero(42, '', '');
// #docregion
// #docregion add-hero
addHero() {
this.hero = new Hero(42, '', '');
this.buildForm();
this.onValueChanged('');
this.onValueChanged();
// #enddocregion add-hero
// #enddocregion class
this.active = false;
setTimeout(() => this.active = true, 0);
// #docregion class
// #docregion
// #docregion add-hero
}
// #enddocregion add-hero
// #docregion form-builder
heroForm: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit(): void {
this.buildForm();
}
//// New with Reactive Form
buildForm(): void {
this.heroForm = this.fb.group({
// #docregion name-validators
'name': [this.hero.name, [
Validators.required,
Validators.minLength(4),
Validators.maxLength(24),
forbiddenNameValidator(/bob/i)
]
],
// #enddocregion name-validators
'alterEgo': [this.hero.alterEgo],
'power': [this.hero.power, Validators.required]
});
heroForm: FormGroup;
constructor(private builder: FormBuilder) { }
this.heroForm.valueChanges
.subscribe(data => this.onValueChanged(data));
}
ngOnInit(): void { this.buildForm(); }
// #enddocregion form-builder
formError = {
onValueChanged(data?: any) {
const controls = this.heroForm ? this.heroForm.controls : {};
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = controls[field];
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
formErrors = {
'name': '',
'power': ''
};
validationMessages = {
'name': {
'required': 'Name is required.',
'minlength': 'Name must be at least 4 characters long.',
'maxlength': 'Name cannot be more than 24 characters long.'
'required': 'Name is required.',
'minlength': 'Name must be at least 4 characters long.',
'maxlength': 'Name cannot be more than 24 characters long.',
'forbiddenName': 'Someone named "Bob" cannot be a hero.'
},
'power': {
'required': 'Power is required.'
}
};
buildForm(): void {
this.heroForm = this.builder.group({
'name': [this.model.name, [
Validators.required,
Validators.minLength(4),
Validators.maxLength(24)
]
],
'alterEgo': [this.model.alterEgo],
'power': [this.model.power, Validators.required]
});
this.heroForm.valueChanges
.subscribe(data => this.onValueChanged(data));
}
onValueChanged(data: any) {
const controls = this.heroForm ? this.heroForm.controls : {};
for (const field in this.formError) {
// clear previous error message (if any)
this.formError[field] = '';
const control = controls[field];
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formError[field] += messages[key] + ' ';
}
}
}
}
isRequired(controlName: string): boolean {
const msgs = this.validationMessages[controlName];
return msgs && msgs['required'];
}
}
// #enddocregion class
// #enddocregion

View File

@ -2,7 +2,7 @@
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { SharedModule } from '../shared/shared.module';
import { SharedModule } from '../shared/shared.module';
import { HeroFormReactiveComponent } from './hero-form-reactive.component';
@NgModule({

View File

@ -0,0 +1,43 @@
// #docregion
import { Directive, Input, OnChanges, SimpleChanges } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn, Validators } from '@angular/forms';
// #docregion custom-validator
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
const name = control.value;
const no = nameRe.test(name);
return no ? {'forbiddenName': {name}} : null;
};
}
// #enddocregion custom-validator
// #docregion directive
@Directive({
selector: '[forbiddenName]',
// #docregion directive-providers
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
// #enddocregion directive-providers
})
export class ForbiddenValidatorDirective implements Validator, OnChanges {
@Input() forbiddenName: string;
private valFn = Validators.nullValidator;
ngOnChanges(changes: SimpleChanges): void {
const change = changes['forbiddenName'];
if (change) {
const val: string | RegExp = change.currentValue;
const re = val instanceof RegExp ? val : new RegExp(val, 'i');
this.valFn = forbiddenNameValidator(re);
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: AbstractControl): {[key: string]: any} {
return this.valFn(control);
}
}
// #docregion directive

View File

@ -1,12 +1,14 @@
// #docregion
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SubmittedComponent } from './submitted.component';
import { ForbiddenValidatorDirective } from './forbidden-name.directive';
import { SubmittedComponent } from './submitted.component';
@NgModule({
imports: [ CommonModule],
declarations: [ SubmittedComponent ],
exports: [ CommonModule, SubmittedComponent ]
declarations: [ ForbiddenValidatorDirective, SubmittedComponent ],
exports: [ ForbiddenValidatorDirective, SubmittedComponent,
CommonModule ]
})
export class SharedModule { }

View File

@ -2,12 +2,13 @@
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../shared/shared.module';
import { HeroFormTemplateComponent } from './hero-form-template.component';
import { SharedModule } from '../shared/shared.module';
import { HeroFormTemplate1Component } from './hero-form-template1.component';
import { HeroFormTemplate2Component } from './hero-form-template2.component';
@NgModule({
imports: [ SharedModule, FormsModule ],
declarations: [ HeroFormTemplateComponent ],
exports: [ HeroFormTemplateComponent ]
declarations: [ HeroFormTemplate1Component, HeroFormTemplate2Component ],
exports: [ HeroFormTemplate1Component, HeroFormTemplate2Component ]
})
export class HeroFormTemplateModule { }

View File

@ -1,28 +1,30 @@
<!-- #docregion -->
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form (Template-Driven)</h1>
<form *ngIf="active"
(ngSubmit)="onSubmit()"
#heroForm="ngForm">
<h1>Hero Form 1 (Template)</h1>
<!-- #docregion form-tag-->
<form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()">
<!-- #enddocregion form-tag-->
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label>
<input type="text" id="name" class="form-control"
required minlength="4" maxlength="24"
name="name" [(ngModel)]="model.name"
name="name" [(ngModel)]="hero.name"
#name="ngModel" >
<div *ngIf="name.errors && (name.dirty || name.touched)"
class="alert alert-danger">
<div [hidden]="!name.errors.required">
Name is required
</div>
<div [hidden]="!name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div [hidden]="!name.errors.maxlength">
Name cannot be more than 24 characters long.
</div>
<div [hidden]="!name.errors.required">
Name is required
</div>
<div [hidden]="!name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div [hidden]="!name.errors.maxlength">
Name cannot be more than 24 characters long.
</div>
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
@ -30,17 +32,19 @@
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" id="alterEgo" class="form-control"
name="alterEgo" [(ngModel)]="model.alterEgo">
name="alterEgo"
[(ngModel)]="hero.alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select id="power" class="form-control"
required
name="power" [(ngModel)]="model.power"
name="power"
[(ngModel)]="hero.power" required
#power="ngModel" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="power.errors && power.touched" class="alert alert-danger">
<div [hidden]="!power.errors.required">Power is required</div>
</div>
@ -49,9 +53,9 @@
<button type="submit" class="btn btn-default"
[disabled]="!heroForm.form.valid">Submit</button>
<button type="button" class="btn btn-default"
(click)="newHero()">New Hero</button>
(click)="addHero()">New Hero</button>
</form>
</div>
<hero-submitted [hero]="model" [(submitted)]="submitted"></hero-submitted>
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
</div>

View File

@ -8,15 +8,15 @@ import { Hero } from '../shared/hero';
@Component({
moduleId: module.id,
selector: 'hero-form-template',
templateUrl: 'hero-form-template.component.html'
selector: 'hero-form-template1',
templateUrl: 'hero-form-template1.component.html'
})
// #docregion class
export class HeroFormTemplateComponent {
export class HeroFormTemplate1Component {
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
model = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
submitted = false;
@ -24,20 +24,23 @@ export class HeroFormTemplateComponent {
this.submitted = true;
}
// #enddocregion class
// #enddocregion
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
active = true;
// #docregion
// #docregion class
newHero() {
this.model = new Hero(42, '', '');
addHero() {
this.hero = new Hero(42, '', '');
// #enddocregion class
// #enddocregion
this.active = false;
setTimeout(() => this.active = true, 0);
// #docregion
// #docregion class
}
}

View File

@ -0,0 +1,52 @@
<!-- #docregion -->
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form 2 (Template & Messages)</h1>
<!-- #docregion form-tag-->
<form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()">
<!-- #enddocregion form-tag-->
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label>
<!-- #docregion name-input -->
<input type="text" id="name" class="form-control"
required minlength="4" maxlength="24" forbiddenName="bob"
name="name" [(ngModel)]="hero.name" >
<!-- #enddocregion name-input -->
<div *ngIf="formErrors.name" class="alert alert-danger">
{{ formErrors.name }}
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" id="alterEgo" class="form-control"
name="alterEgo"
[(ngModel)]="hero.alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select id="power" class="form-control"
name="power"
[(ngModel)]="hero.power" required >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="formErrors.power" class="alert alert-danger">
{{ formErrors.power }}
</div>
</div>
<button type="submit" class="btn btn-default"
[disabled]="!heroForm.form.valid">Submit</button>
<button type="button" class="btn btn-default"
(click)="addHero()">New Hero</button>
</form>
</div>
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
</div>

View File

@ -0,0 +1,99 @@
/* tslint:disable: member-ordering forin */
// #docplaster
// #docregion
import { Component, AfterViewChecked, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Hero } from '../shared/hero';
@Component({
moduleId: module.id,
selector: 'hero-form-template2',
templateUrl: 'hero-form-template2.component.html'
})
export class HeroFormTemplate2Component implements AfterViewChecked {
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
submitted = false;
onSubmit() {
this.submitted = true;
}
// #enddocregion
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
active = true;
// #docregion
addHero() {
this.hero = new Hero(42, '', '');
// #enddocregion
this.active = false;
setTimeout(() => this.active = true, 0);
// #docregion
}
// #docregion view-child
heroForm: NgForm;
@ViewChild('heroForm') currentForm: NgForm;
ngAfterViewChecked() {
this.formChanged();
}
formChanged() {
if (this.currentForm === this.heroForm) { return; }
this.heroForm = this.currentForm;
if (this.heroForm) {
this.heroForm.valueChanges
.subscribe(data => this.onValueChanged(data));
}
}
// #enddocregion view-child
// #docregion handler
onValueChanged(data?: any) {
const controls = this.heroForm ? this.heroForm.controls : {};
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = controls[field];
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
formErrors = {
'name': '',
'power': ''
};
// #enddocregion handler
// #docregion messages
validationMessages = {
'name': {
'required': 'Name is required.',
'minlength': 'Name must be at least 4 characters long.',
'maxlength': 'Name cannot be more than 24 characters long.',
'forbiddenName': 'Someone named "Bob" cannot be a hero.'
},
'power': {
'required': 'Power is required.'
}
};
// #enddocregion messages
}
// #enddocregion

View File

@ -8,7 +8,7 @@
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="forms.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>

View File

@ -1,198 +1,462 @@
include ../_util-fns
<a id="top"></a>
a#top
:marked
We can improve overall data quality by validating user input for accuracy and completeness.
We can improve overall data quality by validating user input for accuracy and completeness.
In this cookbook we show how to validate user input in the UI and display useful validation messages
using first the template-driven forms and then the reactive forms approach.
In this cookbook we show how to validate user input in the UI and display useful validation messages
using first the template-driven forms and then the reactive forms approach.
.l-sub-section
:marked
Learn more about these choices in the [Forms chapter.](../guide/forms.html)
An Angular component consists of a template and a component class containing the code that drives the template.
The first example demonstrates input validation entirely within the template.
The second example moves the validation logic out of the template and into the component class,
giving the developer more control and easier unit testing.
Both examples are based on the the sample form in the [Forms chapter.](../guide/forms.html)
<a id="toc"></a>
a#toc
:marked
## Contents
[Template-Driven Forms Approach](#template-driven)
[Simple Template-Driven Forms](#template1)
[Reactive Forms Approach](#reactive)
[Template-Driven Forms with validation messages in code](#template2)
**Try the live example**
<live-example name="cb-form-validation" embedded img="cookbooks/form-validation/plunker.png"></live-example>
[Reactive Forms with validation in code](#reactive)
[Custom validation](#custom-validation)
[Testing](#testing)
a#live-example
:marked
**Try the live example to see and download the full cookbook source code**
live-example(name="cb-form-validation" embedded img="cookbooks/form-validation/plunker.png")
.l-main-section
<a id="template-driven"></a>
a#template1
:marked
## Template-Driven Forms
## Simple Template-Driven Forms
In the template-driven approach,
each control on the form defines its own validation and validation messages in the template.
In the template-driven approach, you arrange
[form elements](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms_in_HTML) in the component's template.
You add Angular form directives (mostly directives beginning `ng...`) to help
Angular construct a corresponding internal control model that implements form functionality.
We say that the control model is _implicit_ in the template.
To validate user input, you add [HTML validation attributes](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation)
to the elements. Angular interprets those as well, adding validator functions to the control model.
Angular exposes information about the state of the controls including
whether the user has "touched" the control or made changes and if the control values are valid.
In the first template validation example,
we add more HTML to read that control state and update the display appropriately.
Here's an excerpt from the template html for a single input box control bound to the hero name:
+makeExample('cb-form-validation/ts/app/template/hero-form-template.component.html','name-with-error-msg','app/template/hero-form-template.component.html (Hero name)')
+makeExample('cb-form-validation/ts/app/template/hero-form-template1.component.html','name-with-error-msg','template/hero-form-template1.component.html (Hero name)')(format='.')
:marked
Note the following:
- The `<input>` element implements the validation rules as HTML validation attributes: `required`, `minlength`, and `maxlength`.
- The `<input>` element carries the HTML validation attributes: `required`, `minlength`, and `maxlength`.
- Set the `name` attribute of the input box so Angular can track this input element.
- We set the `name` attribute of the input box to `"name"` so Angular can track this input element and associate it
with an Angular form control called `name` in its internal control model.
- The `[(ngModel)]` two-way data binding to the hero's name in the `model.name` property also
registers the input box as a control associated with the implicit `NgForm` directive.
- We use the `[(ngModel)]` directive to two-way data bind the input box to the `hero.name` property.
- A template variable (`#name`) is a reference to this control.
that we can check for control states such as `valid` or `dirty`.
The template variable value is always `ngModel`.
- We set a template variable (`#name`) to the value `"ngModel"` (always `ngModel`).
This gives us a reference to the Angular `NgModel` directive
associated with this control that we can use _in the template_
to check for control states such as `valid` and `dirty`.
- A `<div>` element for a group of validation error messages.
The `*ngIf` reveals the error group if there are any errors and
- The `*ngIf` on `<div>` element reveals a set of nested message `divs` but only if there are "name" errors and
the control is either `dirty` or `touched`.
- Within the error group are separate `<div>` elements for each possible validation error.
Here we've prepared messages for `required`, `minlength`, and `maxlength`.
- Each nested `<div>` can present a custom message for one of the possible validation errors.
We've prepared messages for `required`, `minlength`, and `maxlength`.
The full template repeats this kind of layout for each data entry control on the form.
.l-sub-section
:marked
#### Why check _dirty_ and _touched_?
We shouldn't show errors for a new hero before the user has had a chance to edit the value.
The checks for `dirty` and `touched` prevent premature display of errors.
Learn about `dirty` and `touched` in the [Forms](../guide/forms.html) chapter.
:marked
The component class manages the hero model used in the data binding
as well as other code to support the view.
The component class manages the hero model used in the data binding
as well as other code to support the view.
+makeExample('cb-form-validation/ts/app/template/hero-form-template.component.ts','class','app/template/hero-form-template.component.ts')
+makeExample('cb-form-validation/ts/app/template/hero-form-template1.component.ts','class','template/hero-form-template1.component.ts (class)')
:marked
Use this template-driven validation technique when working with simple forms with simple validation scenarios.
Use this template-driven validation technique when working with static forms with simple, standard validation rules.
Here are the pertinent files for the template-driven approach:
Here are the complete files for the first version of `HeroFormTemplateCompononent` in the template-driven approach:
+makeTabs(
`cb-form-validation/ts/app/template/hero-form-template.module.ts,
cb-form-validation/ts/app/template/hero-form-template.component.html,
cb-form-validation/ts/app/template/hero-form-template.component.ts,
cb-form-validation/ts/app/shared/hero.ts,
cb-form-validation/ts/app/shared/submitted.component.ts`,
+makeTabs(
`cb-form-validation/ts/app/template/hero-form-template1.component.html,
cb-form-validation/ts/app/template/hero-form-template1.component.ts`,
'',
`app/template/hero-form-template.module.ts,
app/template/hero-form-template.component.html,
app/template/hero-form-template.component.ts,
app/shared/hero.ts,
app/shared/submitted.component.ts`)
`template/hero-form-template1.component.html,
template/hero-form-template1.component.ts`)
.l-main-section
<a id="reactive"></a>
a#template2
:marked
## Reactive Forms
## Template-Driven Forms with validation messages in code
Reactive forms are an alternate approach to form validation in the validation rules are specified in the model as
defined in the component class. Defining the validation in the class instead of the template gives you more control.
You can adjust the validation based on the application state or user.
Your code then becomes the source of truth for your validation.
While the layout is straightforward,
there are obvious shortcomings with the way we handle validation messages:
We also remove the data binding (`ngModel`) and validation messages from the template.
This means that we need to set the default value for each control, and we need to add code
that tracks the user's changes so we can hide/show validation messages as needed.
* It takes a lot of HTML to represent all possible error conditions.
This gets out of hand when there are many controls and many validation rules.
* We're not fond of so much JavaScript logic in HTML.
.alert.is-important
* The messages are static strings, hard-coded into the template.
We often require dynamic messages that we should shape in code.
We can move the logic and the messages into the component with a few changes to
the template and component.
Here's the hero name again, excerpted from the revised template ("Template 2"), next to the original version:
+makeTabs(
`cb-form-validation/ts/app/template/hero-form-template2.component.html,
cb-form-validation/ts/app/template/hero-form-template1.component.html`,
'name-with-error-msg, name-with-error-msg',
`hero-form-template2.component.html (name #2),
hero-form-template1.component.html (name #1)`)
:marked
The `<input>` element HTML is almost the same. There are noteworthy differences:
- The hard-code error message `<divs>` are gone.
- There's a new attribute, `forbiddenName`, that is actually a custom validation directive.
It invalidates the control if the user enters "bob" anywhere in the name ([try it](#live-example)).
We discuss [custom validation directives](#custom-validation) later in this cookbook.
- The `#name` template variable is gone because we no longer refer to the Angular control for this element.
- Binding to the new `formErrors.name` property is sufficent to display all name validation error messages.
#### Component class
The original component code stays the same.
We _added_ new code to acquire the Angular form control and compose error messages.
The first step is to acquire the form control that Angular created from the template by querying for it.
Look back at the top of the component template where we set the
`#heroForm` template variable in the `<form>` element:
+makeExample('cb-form-validation/ts/app/template/hero-form-template1.component.html','form-tag','template/hero-form-template1.component.html (form tag)')(format='.')
:marked
The `heroForm` variable is a reference to the control model that Angular derived from the template.
We tell Angular to inject that model into the component class's `currentForm` property using a `@ViewChild` query:
+makeExample('cb-form-validation/ts/app/template/hero-form-template2.component.ts','view-child','template/hero-form-template2.component.ts (heroForm)')(format='.')
:marked
Some observations:
- Angular `@ViewChild` queries for a template variable when you pass it
the name of that variable as a string (`'heroForm'` in this case).
- The `heroForm` object changes several times during the life of the component, most notably when we add a new hero.
We'll have to re-inspect it periodically.
- Angular calls the `ngAfterViewChecked` [lifecycle hook method](../guide/lifecycle-hooks.html#afterview)
when anything changes in the view.
That's the right time to see if there's a new `heroForm` object.
- When there _is_ a new `heroForm` model, we subscribe to its `valueChanged` _Observable_ property.
The `onValueChanged` handler looks for validation errors after every user keystroke.
+makeExample('cb-form-validation/ts/app/template/hero-form-template2.component.ts','handler','template/hero-form-template2.component.ts (handler)')(format='.')
:marked
The `onValueChanged` handler interprets user data entry.
The `data` object passed into the handler contains the current element values.
The handler ignores them. Instead, it iterates over the fields of the component's `formErrors` object.
The `formErrors` is a dictionary of the hero fields that have validation rules and their current error messages.
Only two hero properties have validation rules, `name` and `power`.
The messages are empty strings when the hero data are valid.
For each field, the handler
- clears the prior error message if any
- acquires the field's corresponding Angular form control
- if such a control exists _and_ its been changed ("dirty") _and_ its invalid ...
- the handler composes a consolidated error message for all of the control's errors.
We'll need some error messages of course, a set for each validated property, one message per validation rule:
+makeExample('cb-form-validation/ts/app/template/hero-form-template2.component.ts','messages','template/hero-form-template2.component.ts (messages)')(format='.')
:marked
Now every time the user makes a change, the `onValueChanged` handler checks for validation errors and produces messages accordingly.
### Is this an improvement?
Clearly the template got substantially smaller while the component code got substantially larger.
It's not easy to see the benefit when there are just three fields and only two of them have validation rules.
Consider what happens as we increase the number of validated fields and rules.
In general, HTML is harder to read and maintain than code.
The initial template was already large and threatening to get rapidly worse as we add more validation message `<divs>`.
After moving the validation messaging to the component,
the template grows more slowly and proportionally.
Each field has approximately the same number of lines no matter its number of validation rules.
The component also grows proportionally, at the rate of one line per validated field
and one line per validation message.
Both trends are manageable.
Now that the messages are in code, we have more flexibility. We can compose messages more intelligently.
We can refactor the messages out of the component, perhaps to a service class that retrieves them from the server.
In short, there are more opportunities to improve message handling now that text and logic have moved from template to code.
### _FormModule_ and template-driven forms
Angular has two different forms modules &mdash; `FormsModule` and `ReactiveFormsModule` &mdash;
that correspond with the two approaches to form development.
Both modules come from the same `@angular/forms` library package.
We've been reviewing the "Template-driven" approach which requires the `FormsModule`
Here's how we imported it in the `HeroFormTemplateModule`.
+makeExample('cb-form-validation/ts/app/template/hero-form-template.module.ts','','template/hero-form-template.module.ts')(format='.')
.l-sub-section
:marked
When moving the validation attributes out of the HTML, we are no longer aria ready. Work is being done to
address this.
We haven't talked about the `SharedModule` or its `SubmittedComponent` which appears at the bottom of every
form template in this cookbook.
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts','class','app/reactive/hero-form-reactive.component.ts')
They're not germane to the validation story. Look at the [live example](#live-example) if you're interested.
.l-main-section
a#reactive
:marked
## Reactive Forms
In the template-driven approach, you markup the template with form elements, validation attributes,
and `ng...` directives from the Angular `FormsModule`.
At runtime, Angular interprets the template and derives its _form control model_.
**Reactive Forms** takes a different approach.
You create the form control model in code. You write the template with form elements
and`form...` directives from the Angular `ReactiveFormsModule`.
At runtime, Angular binds the template elements to your control model based on your instructions.
This approach requires a bit more effort. *You have to write the control model and manage it*.
In return, you can
* add, change, and remove validation functions on the fly
* manipulate the control model dynamically from within the component
* [test](#testing) validation and control logic with isolated unit tests.
The third cookbook sample re-writes the hero form in _reactive forms_ style.
### Switch to the _ReactiveFormsModule_
The reactive forms classes and directives come from the Angular `ReactiveFormsModule`, not the `FormsModule`.
The application module for the "Reactive Forms" feature in this sample looks like this:
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts','','app/reactive/hero-form-reactive.module.ts')(format='.')
:marked
The "Reactive Forms" feature module and component are in the `app/reactive` folder.
Let's focus on the `HeroFormReactiveComponent` there, starting with its template.
### Component template
We begin by changing the `<form>` tag so that it binds the Angular `formGroup` directive in the template
to the `heroForm` property in the component class.
The `heroForm` is the control model that the component class builds and maintains.
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.html','form-tag')(format='.')
:marked
Then we modify the template HTML elements to match the _reactive forms_ style.
Here is the "name" portion of the template again, revised for reactive forms and compared with the template-driven version:
+makeTabs(
`cb-form-validation/ts/app/reactive/hero-form-reactive.component.html,
cb-form-validation/ts/app/template/hero-form-template2.component.html`,
'name-with-error-msg, name-with-error-msg',
`hero-form-reactive.component.html (name #3),
hero-form-template1.component.html (name #2)`)
:marked
In the component's class, we define the form and our own data structures to manage definition and display of the validation messages:
- Declare a property for the form typed as a `FormGroup`.
- Declare a property for a collection that contains the *current* validation messages to display to the user.
We'll initialize this collection with one entry for each control. We'll update this collection
with appropriate validation messages when validation rules are broken.
- Declare a property for a collection that contains the set of *possible* validation messages. We'll initialize
this collection with all of the possible validation messages for each control.
- Add a constructor for the class and use dependency injection to inject in the `FormBuilder` service. We use
that service to build the form.
- In the constructor, initialize the collections.
- The `formError` collection is initialized using the control
name as the key and the current validation message as the value. When the form is first displayed, no validation messages
should appear, so the current validation message for each control is empty.
- The `validationMessages` collection is
initialized using the control name as the key and the set of possible validation messages as the value. For this example,
we hard-code in the set of validation messages. But you can retrieve these messages from an external file or
from a database table. Alternatively, you could build a service that retrieved and managed the set of validation messsages.
- Build a method to create the form. We name this method `buildForm` in our example.
The form is created in a method so it can be
called again to reset the form with different default values when adding a new hero.
- In the `buildForm` method, group the controls defined for the form using the `FormBuilder` instance. Here we define each control's name, default value, and
validation rules.
- We call this `buildForm` method from the `ngOnInit` lifecycle hook method. But in many applications, you may need to call `buildForm`
from somewhere else. For example, if the default values are coming from an http request, call the `buildForm` method
after the data is retrieved.
Key changes:
- the validation attributes are gone (except `required`) because we'll be validating in code.
- `required` remains, not for validation purposes (we'll cover that in the code),
but rather for css styling and accessibility.
.l-sub-section
:marked
Learn more about `ngOnInit` in the [LifeCycle Hooks](../guide/lifecycle-hooks.html) chapter.
:marked
There is one more important thing that we need to do. We need to watch for any changes that the user makes and adjust the
validation messages appropriately. For example, if the user enters a single character into the name field, we need to
change the validation message from `Name is required` to `Name must be at least 4 characters long`. And when the user
enters the fourth character, we need to remove the message entirely.
A future version of reactive forms will add the `required` HTML validation attribute to the DOM element
(and perhaps the `aria-required` attribute) when the control has the `required` validator function.
To watch for changes, we add one additional statement to the `buildForm` method. We subscribe to the built-in
`FormGroup`'s `valueChanges` observable. Each time any control on the form is changed by the user, we receive a
notification. In our example, we then call an `onValueChanged` method to reset the validation messages.
In the `onValueChanged` method, we:
- Loop through each entry in the `formError` collection.
- Determine whether the control has an error using the control's properties and our business rules. In our example
we define that a control has an error if the control is dirty and not valid.
- We clear any prior validation messages.
- If the control has a validation error, we loop through the errors collection and concatenate the appropriate
validation messages into one message for display to the user.
We'll use the form and `formError` collection properties in the template. Notice that when using the reactive forms approach,
the amount of code required for each control in the template is signficantly reduced.
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.html','name-with-error-msg','app/reactive/hero-form-reactive.component.html')
Until then, apply the `required` attribute _and_ add the `Validator.required` function
to the control model, as we'll do below.
:marked
In the template, define a standard label and set up an input box for validation as follows:
- Set the `formControlName` directive to the name of the control as defined in the `FormBuilder`'s `group` method, `name` in this example.
- the `formControlName` replaces the `name` attribute; it serves the same
purpose of correlating the input box with the Angular form control.
In this example we also used `ngClass` to set a style on required fields. This is optional and based on the styling
you select for your application.
- the two-way `[(ngModel)]` binding is gone.
The reactive approach does not use data binding to move data into and out of the form controls.
We do that in code.
In the `div` element for the validation messages, we use the validation messages collection (`formError` in this example) to determine whether to display a validation message.
- We use `*ngIf` to check whether the control has a validation message in the collection.
- If so, we display it to the user using interpolation.
.l-sub-section
:marked
The retreat from data binding is a principle of the reactive paradigm rather than a technical limitation.
:marked
### Component class
Repeat for each data entry control on the form.
The component class is now responsible for defining and managing the form control model.
Angular no longer derives the control model from the template so we can no longer query for it.
We create the Angular form control model explicitly with the help of the `FormBuilder`.
The template then has no validation logic. If there is a validation message in the collection it displays it, if not
it doesn't. All of the logic is in the component class.
Here's the section of code devoted to that process, paired with the template-driven code it replaces:
+makeTabs(
`cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts,
cb-form-validation/ts/app/template/hero-form-template2.component.ts`,
'form-builder, view-child',
`reactive/hero-form-reactive.component.ts (FormBuilder),
template/hero-form-template2.component.ts (ViewChild)`)
:marked
- we inject the `FormBuilder` in a constructor.
Use this technique when you want better control over the validation rules and messsages.
Here are the pertinent files for the reactive forms approach:
+makeTabs(
`cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts,
cb-form-validation/ts/app/reactive/hero-form-reactive.component.html,
cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts,
cb-form-validation/ts/app/shared/hero.ts,
cb-form-validation/ts/app/shared/submitted.component.ts`,
'',
`app/reactive/hero-form-reactive.module.ts,
app/reactive/hero-form-reactive.component.html,
app/reactive/hero-form-reactive.component.ts,
app/shared/hero.ts,
app/shared/submitted.component.ts`)
- we call a `buildForm` method in the `ngOnInit` [lifecycle hook method](../guide/lifecycle-hooks.html#hooks-overview)
because that's when we'll have the hero data. We'll call it again in the `addHero` method.
.l-sub-section
:marked
A real app would retrieve the hero asynchronously from a data service, a task best performed in the `ngOnInit` hook.
:marked
- the `buildForm` method uses the `FormBuilder` (`fb`) to declare the form control model.
Then it attaches the same `onValueChanged` handler to the form.
:marked
[Back to top](#top)
#### _FormBuilder_ declaration
The `FormBuilder` declaration object specifies the three controls of the sample's hero form.
Each control spec is a control name with an array value.
The first array element is the current value of the corresponding hero field.
The (optional) second value is a validator function or an array of validator functions.
Most of the validator functions are stock validators provided by Angular as static methods of the `Validators` class.
Angular has stock validators that correspond to the standard HTML validation attributes.
The `forbiddenNames` validator on the `"name"` control is a custom validator,
discussed in a separate [section below](#custom-validation).
.l-sub-section
:marked
Learn more about `FormBuilder` in a _forthcoming_ chapter on reactive forms.
:marked
#### Committing hero value changes
In two-way data binding, the user's changes flow automatically from the controls back to the data model properties.
Reactive forms do not use data binding to update data model properties.
The developer decides _when and how_ to update the data model from control values.
This sample updates the model twice:
1. when the user submits the form
1. when the user chooses to add a new hero
The `onSubmit` method simply replaces the `hero` object with the combined values of the form:
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts','on-submit')(format='.')
.l-sub-section
:marked
This example is "lucky" in that the `heroForm.value` properties _just happen_ to
correspond _exactly_ to the hero data object properties.
:marked
The `addHero` method discards pending changes and creates a brand new `hero` model object.
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts','add-hero')(format='.')
:marked
Then it calls `buildForm` again which replaces the previous `heroForm` control model with a new one.
The `<form>` tag's `[formGroup]` binding refreshes the page with the new control model.
Finally, it calls the `onValueChanged` handler to clear previous error messages and reset them
to reflect Angular's validation of the new `hero` object.
Here's the complete reactive component file, compared to the two template-driven component files.
+makeTabs(
`cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts,
cb-form-validation/ts/app/template/hero-form-template2.component.ts,
cb-form-validation/ts/app/template/hero-form-template1.component.ts`,
'',
`reactive/hero-form-reactive.component.ts (#3),
template/hero-form-template2.component.ts (#2),
template/hero-form-template1.component.ts (#1)`)
.l-sub-section
:marked
Run the [live example](#live-example) to see how the reactive form behaves
and to compare all of the files in this cookbook sample.
.l-main-section
a#custom-validation
:marked
## Custom validation
This cookbook sample has a custom `forbiddenNamevalidator` function that's applied to both the
template-driven and the reactive form controls. It's in the `app/shared` folder
and declared in the `SharedModule`.
Here's the `forbiddenNamevalidator` function itself:
+makeExample('cb-form-validation/ts/app/shared/forbidden-name.directive.ts','custom-validator', 'shared/forbidden-name.directive.ts (forbiddenNameValidator)')(format='.')
:marked
The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name
and returns a validator function.
In this sample, the forbidden name is "bob";
the validator rejects any hero name containing "bob".
Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
The `forbiddenNamevalidator` factory returns the configured validator function.
That function takes an Angular control object and returns _either_
null if the control value is valid _or_ a validation error object.
The validation error object typically has a property whose name is the validation key ('forbiddenName')
and whose value is an arbitrary dictionary of values that we could insert into an error message (`{name}`).
.l-sub-section
:marked
Learn more about validator functions in a _forthcoming_ chapter on custom form validation.
:marked
#### Custom validation directive
In the reactive forms component we added a configured `forbiddenNamevalidator`
to the bottom of the `'name'` control's validator function list.
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts','name-validators', 'reactive/hero-form-reactive.component.ts (name validators)')(format='.')
:marked
In the template-driven component template, we add the selector (`forbiddenName`) of a custom _attribute directive_ to the name's input box
and configured it to reject "bob".
+makeExample('cb-form-validation/ts/app/template/hero-form-template2.component.html','name-input', 'template/hero-form-template2.component.html (name input)')(format='.')
:marked
The corresponding `ForbiddenValidatorDirective` is a wrapper around the `forbiddenNamevalidator`.
Angular forms recognizes the directive's role in the validation process because the directive registers itself
with the `NG_VALIDATORS` provider, a provider with an extensible collection of validation directives.
+makeExample('cb-form-validation/ts/app/shared/forbidden-name.directive.ts','directive-providers', 'shared/forbidden-name.directive.ts (providers)')(format='.')
:marked
The rest of the directive is unremarkable and we present it here without further comment.
+makeExample('cb-form-validation/ts/app/shared/forbidden-name.directive.ts','directive', 'shared/forbidden-name.directive.ts (directive)')
:marked
.l-sub-section
:marked
See the [Attribute Directives](../guide/attribute-directives.html) chapter.
.l-main-section
a#testing
:marked
## Testing Considerations
We can write _isolated unit tests_ of validation and control logic in _Reactive Forms_.
_Isolated unit tests_ probe the component class directly, independent of its
interactions with its template, the DOM, other dependencies, or Angular itself.
Such tests have minimal setup, are quick to write, and easy to maintain.
They do not require the `Angular TestBed` or asynchronous testing practices.
That's not possible with _Template-driven_ forms.
The template-driven approach relies on Angular to produce the control model and
to derive validation rules from the HTML validation attributes.
You must use the `Angular TestBed` to create component test instances,
write asynchronous tests, and interact with the DOM.
While not difficult, this takes more time, work and skill &mdash;
factors that tend to diminish test code coverage and quality.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 21 KiB