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:
parent
f971685a7c
commit
1212b5147f
|
@ -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 () {
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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 { }
|
||||
|
|
|
@ -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 { }
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 — `FormsModule` and `ReactiveFormsModule` —
|
||||
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 —
|
||||
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 |
Loading…
Reference in New Issue