diff --git a/public/docs/_examples/cb-form-validation/e2e-spec.ts b/public/docs/_examples/cb-form-validation/e2e-spec.ts index 5e68d6e657..60fe5ae0f3 100644 --- a/public/docs/_examples/cb-form-validation/e2e-spec.ts +++ b/public/docs/_examples/cb-form-validation/e2e-spec.ts @@ -1,4 +1,5 @@ /// +'use strict'; // necessary for node! describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () { beforeEach(function () { diff --git a/public/docs/_examples/cb-form-validation/ts/app/app.component.ts b/public/docs/_examples/cb-form-validation/ts/app/app.component.ts index 5260b8d9e1..2da4dc4d0a 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/app.component.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/app.component.ts @@ -3,8 +3,10 @@ import { Component } from '@angular/core'; @Component({ selector: 'my-app', - template: ` + template: `
- ` + +
+ ` }) export class AppComponent { } diff --git a/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.html b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.html index 8b7f81f0d7..149537bd3e 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.html +++ b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.html @@ -1,16 +1,19 @@
-

Hero Form (Reactive)

-
+

Hero Form 3 (Reactive)

+ + +
+ -
- {{ formError.name }} + formControlName="name" required > + +
+ {{ formErrors.name }}
@@ -18,28 +21,27 @@
+ formControlName="alterEgo" >
-
- {{ formError.power }} + +
+ {{ formErrors.power }}
+ (click)="addHero()">New Hero
- +
diff --git a/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts index c61d3ef05d..f33d37bb13 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts @@ -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 diff --git a/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts index dd9aecda74..6ff9265e92 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts @@ -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({ diff --git a/public/docs/_examples/cb-form-validation/ts/app/shared/forbidden-name.directive.ts b/public/docs/_examples/cb-form-validation/ts/app/shared/forbidden-name.directive.ts new file mode 100644 index 0000000000..a8f6c03d33 --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/shared/forbidden-name.directive.ts @@ -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 + diff --git a/public/docs/_examples/cb-form-validation/ts/app/shared/shared.module.ts b/public/docs/_examples/cb-form-validation/ts/app/shared/shared.module.ts index fd6c35ef2e..2b0ada59bd 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/shared/shared.module.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/shared/shared.module.ts @@ -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 { } diff --git a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.module.ts b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.module.ts index 800ba19643..042c019d5e 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.module.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.module.ts @@ -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 { } diff --git a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.html b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template1.component.html similarity index 60% rename from public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.html rename to public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template1.component.html index 575036701e..22b374b622 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.html +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template1.component.html @@ -1,28 +1,30 @@
-

Hero Form (Template-Driven)

-
+

Hero Form 1 (Template)

+ + +
+ +
-
- Name is required -
-
- Name must be at least 4 characters long. -
-
- Name cannot be more than 24 characters long. -
+
+ Name is required +
+
+ Name must be at least 4 characters long. +
+
+ Name cannot be more than 24 characters long. +
@@ -30,17 +32,19 @@
+ name="alterEgo" + [(ngModel)]="hero.alterEgo" >
+
Power is required
@@ -49,9 +53,9 @@ + (click)="addHero()">New Hero
- +
diff --git a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.ts b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template1.component.ts similarity index 71% rename from public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.ts rename to public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template1.component.ts index 9149ad3126..845fcc9abc 100644 --- a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template1.component.ts @@ -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 } } diff --git a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.html b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.html new file mode 100644 index 0000000000..8bb7066541 --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.html @@ -0,0 +1,52 @@ + +
+
+

Hero Form 2 (Template & Messages)

+ +
+ +
+ + + + + + + +
+ {{ formErrors.name }} +
+ +
+ +
+ + +
+ +
+ + + +
+ {{ formErrors.power }} +
+
+ + + +
+
+ + +
diff --git a/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts new file mode 100644 index 0000000000..ae6c0367b4 --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts @@ -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 diff --git a/public/docs/_examples/cb-form-validation/ts/index.html b/public/docs/_examples/cb-form-validation/ts/index.html index 43a3a70f60..6aea5beaa4 100644 --- a/public/docs/_examples/cb-form-validation/ts/index.html +++ b/public/docs/_examples/cb-form-validation/ts/index.html @@ -8,7 +8,7 @@ - + diff --git a/public/docs/ts/latest/cookbook/form-validation.jade b/public/docs/ts/latest/cookbook/form-validation.jade index ba0e9693de..877026adcc 100644 --- a/public/docs/ts/latest/cookbook/form-validation.jade +++ b/public/docs/ts/latest/cookbook/form-validation.jade @@ -1,198 +1,462 @@ include ../_util-fns - +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#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** - + [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#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 `` element implements the validation rules as HTML validation attributes: `required`, `minlength`, and `maxlength`. + - The `` 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 `
` element for a group of validation error messages. - The `*ngIf` reveals the error group if there are any errors and + - The `*ngIf` on `
` 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 `
` elements for each possible validation error. - Here we've prepared messages for `required`, `minlength`, and `maxlength`. + - Each nested `
` 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#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 `` element HTML is almost the same. There are noteworthy differences: + - The hard-code error message `` 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 `
` 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 ``. + + 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 `` 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 `` 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. diff --git a/public/resources/images/cookbooks/form-validation/plunker.png b/public/resources/images/cookbooks/form-validation/plunker.png index 0df5840a52..c1fd3eed09 100644 Binary files a/public/resources/images/cookbooks/form-validation/plunker.png and b/public/resources/images/cookbooks/form-validation/plunker.png differ