From 3b4a5d533d4c9a0595bbdab676382ebe23a89fce Mon Sep 17 00:00:00 2001 From: Deborah Kurata Date: Wed, 1 Jun 2016 11:54:30 -0700 Subject: [PATCH 1/3] docs(cb-form-validation): create Form Validation Cookbook --- .../docs/_examples/cb-validation/e2e-spec.ts | 63 ++++++ .../cb-validation/ts/app/app.component.ts | 12 ++ .../cb-validation/ts/app/app.module.ts | 25 +++ .../ts/app/hero-form-model.component.html | 61 ++++++ .../ts/app/hero-form-model.component.ts | 107 ++++++++++ .../ts/app/hero-form-template.component.html | 73 +++++++ .../ts/app/hero-form-template.component.ts | 38 ++++ .../_examples/cb-validation/ts/app/hero.ts | 13 ++ .../_examples/cb-validation/ts/app/main.ts | 8 + .../cb-validation/ts/example-config.json | 0 .../docs/_examples/cb-validation/ts/forms.css | 11 + .../_examples/cb-validation/ts/index.html | 29 +++ .../_examples/cb-validation/ts/plnkr.json | 7 + public/docs/ts/latest/cookbook/_data.json | 5 + .../docs/ts/latest/cookbook/validation.jade | 189 ++++++++++++++++++ 15 files changed, 641 insertions(+) create mode 100644 public/docs/_examples/cb-validation/e2e-spec.ts create mode 100644 public/docs/_examples/cb-validation/ts/app/app.component.ts create mode 100644 public/docs/_examples/cb-validation/ts/app/app.module.ts create mode 100644 public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html create mode 100644 public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts create mode 100644 public/docs/_examples/cb-validation/ts/app/hero-form-template.component.html create mode 100644 public/docs/_examples/cb-validation/ts/app/hero-form-template.component.ts create mode 100644 public/docs/_examples/cb-validation/ts/app/hero.ts create mode 100644 public/docs/_examples/cb-validation/ts/app/main.ts create mode 100644 public/docs/_examples/cb-validation/ts/example-config.json create mode 100644 public/docs/_examples/cb-validation/ts/forms.css create mode 100644 public/docs/_examples/cb-validation/ts/index.html create mode 100644 public/docs/_examples/cb-validation/ts/plnkr.json create mode 100644 public/docs/ts/latest/cookbook/validation.jade diff --git a/public/docs/_examples/cb-validation/e2e-spec.ts b/public/docs/_examples/cb-validation/e2e-spec.ts new file mode 100644 index 0000000000..5e68d6e657 --- /dev/null +++ b/public/docs/_examples/cb-validation/e2e-spec.ts @@ -0,0 +1,63 @@ +/// +describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () { + + beforeEach(function () { + browser.get(''); + }); + + it('should display correct title', function () { + expect(element.all(by.css('h1')).get(0).getText()).toEqual('Hero Form'); + }); + + + it('should not display message before submit', function () { + let ele = element(by.css('h2')); + expect(ele.isDisplayed()).toBe(false); + }); + + it('should hide form after submit', function () { + let ele = element.all(by.css('h1')).get(0); + expect(ele.isDisplayed()).toBe(true); + let b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(ele.isDisplayed()).toBe(false); + }); + }); + + it('should display message after submit', function () { + let b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(element(by.css('h2')).getText()).toContain('You submitted the following'); + }); + }); + + it('should hide form after submit', function () { + let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + expect(alterEgoEle.isDisplayed()).toBe(true); + let submitButtonEle = element.all(by.css('button[type=submit]')).get(0); + submitButtonEle.click().then(function() { + expect(alterEgoEle.isDisplayed()).toBe(false); + }); + }); + + it('should reflect submitted data after submit', function () { + let test = 'testing 1 2 3'; + let newValue: string; + let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + alterEgoEle.getAttribute('value').then(function(value) { + // alterEgoEle.sendKeys(test); + sendKeys(alterEgoEle, test); + newValue = value + test; + expect(alterEgoEle.getAttribute('value')).toEqual(newValue); + }).then(function() { + let b = element.all(by.css('button[type=submit]')).get(0); + return b.click(); + }).then(function() { + let alterEgoTextEle = element(by.cssContainingText('div', 'Alter Ego')); + expect(alterEgoTextEle.isPresent()).toBe(true, 'cannot locate "Alter Ego" label'); + let divEle = element(by.cssContainingText('div', newValue)); + expect(divEle.isPresent()).toBe(true, 'cannot locate div with this text: ' + newValue); + }); + }); +}); + diff --git a/public/docs/_examples/cb-validation/ts/app/app.component.ts b/public/docs/_examples/cb-validation/ts/app/app.component.ts new file mode 100644 index 0000000000..1dcd5a9d5a --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/app.component.ts @@ -0,0 +1,12 @@ +// #docplaster +// #docregion +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-app', + template: ` +
+ ` +}) +export class AppComponent { } +// #enddocregion \ No newline at end of file diff --git a/public/docs/_examples/cb-validation/ts/app/app.module.ts b/public/docs/_examples/cb-validation/ts/app/app.module.ts new file mode 100644 index 0000000000..d20f416716 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/app.module.ts @@ -0,0 +1,25 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { AppComponent } from './app.component'; +import { HeroFormTemplateComponent } from './hero-form-template.component' +import { HeroFormModelComponent } from './hero-form-model.component' + +@NgModule({ + imports: [ + BrowserModule, + FormsModule, + ReactiveFormsModule + ], + declarations: [ + AppComponent, + HeroFormTemplateComponent, + HeroFormModelComponent + ], + bootstrap: [ AppComponent ] +}) +export class AppModule { } +// #enddocregion diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html b/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html new file mode 100644 index 0000000000..9dc4b65635 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html @@ -0,0 +1,61 @@ + + +
+
+

Hero Form (Model-Driven)

+
+
+ + + +
+ {{ formError.name }} +
+ +
+ +
+ + +
+ +
+ + +
+ {{ formError.power }} +
+
+ + + +
+
+ +
+

You submitted the following:

+
+
Name
+
{{ model.name }}
+
+
+
Alter Ego
+
{{ model.alterEgo }}
+
+
+
Power
+
{{ model.power }}
+
+
+ +
+
+ diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts b/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts new file mode 100644 index 0000000000..11037c7dd3 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts @@ -0,0 +1,107 @@ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; + +import { Hero } from './hero'; + +@Component({ + selector: 'hero-form-model', + templateUrl: 'app/hero-form-model.component.html' +}) +// #docregion class +export class HeroFormModelComponent implements OnInit { + heroForm: FormGroup; + formError: { [id: string]: string }; + private validationMessages: { [id: string]: { [id: string]: string } }; + + powers = ['Really Smart', 'Super Flexible', + 'Super Hot', 'Weather Changer']; + + model = new Hero(18, 'Dr IQ', this.powers[0], + 'Chuck Overstreet'); + +// #enddocregion class + submitted = false; +// #docregion class + constructor(private fb: FormBuilder) { + this.formError = { + 'name': '', + 'alterEgo': '', + 'power': '' + }; + this.validationMessages = { + 'name': { + 'required': 'Name is required.', + 'minlength': 'Name must be at least 4 characters long.', + 'maxlength': 'Name cannot be more than 24 characters long.' + }, + 'power': { + 'required': 'Power is required.' + } + }; + } + + ngOnInit(): void { + this.buildForm(); + } + + buildForm(): void { + this.heroForm = this.fb.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) { + for (let field in this.formError) { + if (this.formError.hasOwnProperty(field)) { + let hasError = (this.heroForm.controls[field].dirty) && + !this.heroForm.controls[field].valid; + this.formError[field] = ''; + if (hasError) { + for (let key in this.heroForm.controls[field].errors) { + if (this.heroForm.controls[field].errors.hasOwnProperty(key)) { + this.formError[field] += this.validationMessages[field][key] + ' '; + } + } + } + } + } + } +// #enddocregion class + onSubmit() { + this.submitted = true; + this.model = this.heroForm.value; + } + + isRequired(controlName: string): boolean { + if (Object.keys(this.validationMessages).includes(controlName)) { + return Object.keys(this.validationMessages[controlName]).includes('required');} + return false; + } + // 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; + + newHero() { + this.model = new Hero(42, '', ''); + this.buildForm(); + this.onValueChanged(''); + this.active = false; + setTimeout(() => this.active = true, 0); + } + // #docregion class +} +// #enddocregion class +// #enddocregion diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.html b/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.html new file mode 100644 index 0000000000..e18f72c4c4 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.html @@ -0,0 +1,73 @@ + + +
+
+

Hero Form (Template-Driven)

+
+
+ + + +
+
+ Name is required +
+
+ Name must be at least 4 characters long. +
+
+ Name cannot be more than 24 characters long. +
+
+ +
+ +
+ + +
+ +
+ + +
+
Power is required
+
+
+ + + +
+
+ +
+

You submitted the following:

+
+
Name
+
{{ model.name }}
+
+
+
Alter Ego
+
{{ model.alterEgo }}
+
+
+
Power
+
{{ model.power }}
+
+
+ +
+
+ diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.ts b/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.ts new file mode 100644 index 0000000000..d0a76ce246 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.ts @@ -0,0 +1,38 @@ +// #docplaster +// #docregion +import { Component } from '@angular/core'; + +import { Hero } from './hero'; + +@Component({ + selector: 'hero-form-template', + templateUrl: 'app/hero-form-template.component.html' +}) +// #docregion class +export class HeroFormTemplateComponent { + + powers = ['Really Smart', 'Super Flexible', + 'Super Hot', 'Weather Changer']; + + model = new Hero(18, 'Dr IQ', this.powers[0], + 'Chuck Overstreet'); +// #enddocregion class + submitted = false; + + onSubmit() { this.submitted = true; } + + // 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; + + newHero() { + this.model = new Hero(42, '', ''); + this.active = false; + setTimeout(()=> this.active=true, 0); + } +// #docregion class +} +// #enddocregion class +// #enddocregion diff --git a/public/docs/_examples/cb-validation/ts/app/hero.ts b/public/docs/_examples/cb-validation/ts/app/hero.ts new file mode 100644 index 0000000000..3556a791c1 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/hero.ts @@ -0,0 +1,13 @@ +// #docplaster +// #docregion +export class Hero { + + constructor( + public id: number, + public name: string, + public power: string, + public alterEgo?: string + ) { } + +} +// #enddocregion diff --git a/public/docs/_examples/cb-validation/ts/app/main.ts b/public/docs/_examples/cb-validation/ts/app/main.ts new file mode 100644 index 0000000000..eea33fea8e --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/app/main.ts @@ -0,0 +1,8 @@ +// #docplaster +// #docregion +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); +// #enddocregion \ No newline at end of file diff --git a/public/docs/_examples/cb-validation/ts/example-config.json b/public/docs/_examples/cb-validation/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/cb-validation/ts/forms.css b/public/docs/_examples/cb-validation/ts/forms.css new file mode 100644 index 0000000000..e95fd4bf8d --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/forms.css @@ -0,0 +1,11 @@ +.ng-valid[required] { + border-left: 5px solid #42A948; /* green */ +} + +.ng-invalid { + border-left: 5px solid #a94442; /* red */ +} + +.ng-valid.required { + border-left: 5px solid #42A948; /* green */ +} \ No newline at end of file diff --git a/public/docs/_examples/cb-validation/ts/index.html b/public/docs/_examples/cb-validation/ts/index.html new file mode 100644 index 0000000000..43a3a70f60 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/index.html @@ -0,0 +1,29 @@ + + + Hero Form with Validation + + + + + + + + + + + + + + + + + + + + + Loading... + + + diff --git a/public/docs/_examples/cb-validation/ts/plnkr.json b/public/docs/_examples/cb-validation/ts/plnkr.json new file mode 100644 index 0000000000..aa676ad767 --- /dev/null +++ b/public/docs/_examples/cb-validation/ts/plnkr.json @@ -0,0 +1,7 @@ +{ + "description": "Validation", + "files":[ + "!**/*.d.ts", + "!**/*.js" + ] +} \ No newline at end of file diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index f82d816cef..4cc4187bc7 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -48,6 +48,11 @@ "intro": "Migrate your RC4 app to RC5 in minutes." }, + "validation": { + "title": "Validation", + "intro": "Validate user's form entries" + }, + "set-document-title": { "title": "Set the Document Title", "intro": "Setting the document or window title using the Title service." diff --git a/public/docs/ts/latest/cookbook/validation.jade b/public/docs/ts/latest/cookbook/validation.jade new file mode 100644 index 0000000000..4cccdbbb13 --- /dev/null +++ b/public/docs/ts/latest/cookbook/validation.jade @@ -0,0 +1,189 @@ +include ../_util-fns + + +:marked + We want our data to be accurate and complete. By helping the user enter + appropriate data and confirming that data is valid, we can improve the quality of that incoming data. + + In this cookbook we show how to validate data and display useful validation messages using first the + template-driven forms and then the reactive forms approach. + + An Angular component is comprised of a template and a component class containing the code that drives the template. + The first example demonstrates how to validate data using only the template. The second technique + moves the validation logic out of the template and into the component class, giving you more control and better unit testing. + + In these examples we use the form created in the [Forms chapter.](../guide/forms.html) + + +:marked + ## Table of contents + + [Template-Driven Forms Approach](#template-driven) + + [Reactive Forms Approach](#model-driven) + +:marked + **See the [live example](/resources/live-examples/cb-validation/ts/plnkr.html)**. + +.l-main-section + +:marked + ## Template-Driven Forms Approach + + Using the template-driven approach to form validation, the validation is defined in the template. + Each control on the form defines its validation, binding, and validation messages in the template. + ++makeExample('cb-validation/ts/app/hero-form-template.component.html','name-with-error-msg','app/hero-form-template.component.html') + +:marked + Here we define a standard label and set up an input box for validation of the hero name as follows: + - Add the desired HTML validation attributes. In this example, + we add `required`, `minlength`, and `maxlength` attributes on the input box to define the validation rules. + - Set the `name` attribute of the input box. This is required for Angular to track this input element. + - Use `ngModel` binding to set the default value and track the user's changes to the input box. + This registers the input box as a control that is associated with the `ngForm` directive. + - Define a template reference variable (`name` in this example) that references the registered control. + We use this variable to reference this control when checking the control state, such as `valid` or `dirty`. + + Next we define a `div` element for the validation error messages. + We use the template reference variable to determine whether to display one of the validation messages. + - We use `*ngIf` to check whether the control has errors and whether it is `dirty` or `touched` before + displaying the validation block `div`. + - We then have a separate `div` for each possible validation error. In our example, we defined validation for + `required`, `minlength`, and `maxlength`, so we add one `div` for each one. Each `div` is marked with `[hidden]` + so the validation message is hidden unless the control has the specified error. + + Repeat for each data entry control on the form. +.l-sub-section + :marked + Adding the check for `dirty` or `touched` prevents display of errors before the user has a chance to edit the + value. This is most useful when adding new data, such as a new hero. + + Learn about `dirty` and `touched` in the [Forms](../guide/forms.html) chapter. +:marked + The component class manages the model used in the data binding. And provides any other code required by the view. + ++makeExample('cb-validation/ts/app/hero-form-template.component.ts','class','app/hero-form-template.component.ts') + +:marked + Use this template-driven validation technique when working with simple forms with simple validation scenarios. + + Here's the complete solution for the template-driven approach: + ++makeTabs( + `cb-validation/ts/app/main.ts, + cb-validation/ts/app/app.module.ts, + cb-validation/ts/app/app.component.ts, + cb-validation/ts/app/hero.ts, + cb-validation/ts/app/hero-form-template.component.html, + cb-validation/ts/app/hero-form-template.component.ts`, + '', + 'app/main.ts, app/app.module.ts, app/app.component.ts, app/hero.ts, app/hero-form-template.component.html, app/hero-form-template.component.ts' ) + +.l-main-section + +:marked + ## Reactive Forms Approach + + The alternate way to implement forms in Angular is to use reactive forms, previously called `model-driven forms`. + Using the reactive forms approach to form validation, 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. + + 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. + +.alert.is-important + :marked + When moving the validation attributes out of the HTML, we are no longer aria ready. Work is being done to + address this. + ++makeExample('cb-validation/ts/app/hero-form-model.component.ts','class','app/hero-form-model.component.ts') + +: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. + +.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. + + 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-validation/ts/app/hero-form-model.component.html','name-with-error-msg','app/hero-form-model.component.html') + +: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. + + 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. + + 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. + + Repeat for each data entry control on the form. + + 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. + + Use this technique when you want better control over the validation rules and messsages. + + Here's the complete solution for the reactive forms approach: + ++makeTabs( + `cb-validation/ts/app/main.ts, + cb-validation/ts/app/app.module.ts, + cb-validation/ts/app/app.component.ts, + cb-validation/ts/app/hero.ts, + cb-validation/ts/app/hero-form-model.component.html, + cb-validation/ts/app/hero-form-model.component.ts`, + '', + 'app/main.ts, app/app.module.ts, app/app.component.ts, app/hero.ts, app/hero-form-model.component.html, app/hero-form-model.component.ts' ) + +:marked + [Back to top](#top) \ No newline at end of file From f971685a7c0267ce05bd83611e43b8e1e2e178a5 Mon Sep 17 00:00:00 2001 From: Ward Bell Date: Fri, 26 Aug 2016 21:02:32 -0700 Subject: [PATCH 2/3] docs(cb-form-validation): ward's tweaks renamed from cb-validation to cb-form-validation other refactorings and text changes --- .../e2e-spec.ts | 0 .../ts/app/app.component.ts | 4 +- .../cb-form-validation/ts/app/app.module.ts | 18 +++ .../ts/app/main.ts | 2 - .../hero-form-reactive.component.html | 45 ++++++ .../reactive/hero-form-reactive.component.ts | 107 ++++++++++++++ .../app/reactive/hero-form-reactive.module.ts | 13 ++ .../ts/app/shared}/hero.ts | 4 - .../ts/app/shared/shared.module.ts | 12 ++ .../ts/app/shared/submitted.component.ts | 32 +++++ .../hero-form-template.component.html | 34 ++--- .../template}/hero-form-template.component.ts | 27 ++-- .../app/template/hero-form-template.module.ts | 13 ++ .../ts/example-config.json | 0 .../ts/forms.css | 0 .../ts/index.html | 0 .../ts/plnkr.json | 0 .../cb-validation/ts/app/app.module.ts | 25 ---- .../ts/app/hero-form-model.component.html | 61 -------- .../ts/app/hero-form-model.component.ts | 107 -------------- public/docs/dart/latest/cookbook/_data.json | 6 + .../dart/latest/cookbook/dynamic-form.jade | 2 +- .../dart/latest/cookbook/form-validation.jade | 1 + public/docs/js/latest/cookbook/_data.json | 5 + .../js/latest/cookbook/form-validation.jade | 1 + public/docs/ts/latest/cookbook/_data.json | 10 +- .../{validation.jade => form-validation.jade} | 131 ++++++++++-------- .../cookbooks/form-validation/plunker.png | Bin 0 -> 25379 bytes 28 files changed, 356 insertions(+), 304 deletions(-) rename public/docs/_examples/{cb-validation => cb-form-validation}/e2e-spec.ts (100%) rename public/docs/_examples/{cb-validation => cb-form-validation}/ts/app/app.component.ts (71%) create mode 100644 public/docs/_examples/cb-form-validation/ts/app/app.module.ts rename public/docs/_examples/{cb-validation => cb-form-validation}/ts/app/main.ts (85%) create mode 100644 public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.html create mode 100644 public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts create mode 100644 public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts rename public/docs/_examples/{cb-validation/ts/app => cb-form-validation/ts/app/shared}/hero.ts (82%) create mode 100644 public/docs/_examples/cb-form-validation/ts/app/shared/shared.module.ts create mode 100644 public/docs/_examples/cb-form-validation/ts/app/shared/submitted.component.ts rename public/docs/_examples/{cb-validation/ts/app => cb-form-validation/ts/app/template}/hero-form-template.component.html (64%) rename public/docs/_examples/{cb-validation/ts/app => cb-form-validation/ts/app/template}/hero-form-template.component.ts (58%) create mode 100644 public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.module.ts rename public/docs/_examples/{cb-validation => cb-form-validation}/ts/example-config.json (100%) rename public/docs/_examples/{cb-validation => cb-form-validation}/ts/forms.css (100%) rename public/docs/_examples/{cb-validation => cb-form-validation}/ts/index.html (100%) rename public/docs/_examples/{cb-validation => cb-form-validation}/ts/plnkr.json (100%) delete mode 100644 public/docs/_examples/cb-validation/ts/app/app.module.ts delete mode 100644 public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html delete mode 100644 public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts create mode 100644 public/docs/dart/latest/cookbook/form-validation.jade create mode 100644 public/docs/js/latest/cookbook/form-validation.jade rename public/docs/ts/latest/cookbook/{validation.jade => form-validation.jade} (56%) create mode 100644 public/resources/images/cookbooks/form-validation/plunker.png diff --git a/public/docs/_examples/cb-validation/e2e-spec.ts b/public/docs/_examples/cb-form-validation/e2e-spec.ts similarity index 100% rename from public/docs/_examples/cb-validation/e2e-spec.ts rename to public/docs/_examples/cb-form-validation/e2e-spec.ts diff --git a/public/docs/_examples/cb-validation/ts/app/app.component.ts b/public/docs/_examples/cb-form-validation/ts/app/app.component.ts similarity index 71% rename from public/docs/_examples/cb-validation/ts/app/app.component.ts rename to public/docs/_examples/cb-form-validation/ts/app/app.component.ts index 1dcd5a9d5a..5260b8d9e1 100644 --- a/public/docs/_examples/cb-validation/ts/app/app.component.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/app.component.ts @@ -1,4 +1,3 @@ -// #docplaster // #docregion import { Component } from '@angular/core'; @@ -6,7 +5,6 @@ import { Component } from '@angular/core'; selector: 'my-app', template: `
- ` + ` }) export class AppComponent { } -// #enddocregion \ No newline at end of file diff --git a/public/docs/_examples/cb-form-validation/ts/app/app.module.ts b/public/docs/_examples/cb-form-validation/ts/app/app.module.ts new file mode 100644 index 0000000000..72b4e3a770 --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/app.module.ts @@ -0,0 +1,18 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { HeroFormTemplateModule } from './template/hero-form-template.module'; +import { HeroFormReactiveModule } from './reactive/hero-form-reactive.module'; + +@NgModule({ + imports: [ + BrowserModule, + HeroFormTemplateModule, + HeroFormReactiveModule + ], + declarations: [ AppComponent ], + bootstrap: [ AppComponent ] +}) +export class AppModule { } diff --git a/public/docs/_examples/cb-validation/ts/app/main.ts b/public/docs/_examples/cb-form-validation/ts/app/main.ts similarity index 85% rename from public/docs/_examples/cb-validation/ts/app/main.ts rename to public/docs/_examples/cb-form-validation/ts/app/main.ts index eea33fea8e..961a226688 100644 --- a/public/docs/_examples/cb-validation/ts/app/main.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/main.ts @@ -1,8 +1,6 @@ -// #docplaster // #docregion import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule); -// #enddocregion \ No newline at end of file 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 new file mode 100644 index 0000000000..8b7f81f0d7 --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.html @@ -0,0 +1,45 @@ + +
+
+

Hero Form (Reactive)

+
+
+ + + +
+ {{ formError.name }} +
+ +
+ +
+ + +
+ +
+ + +
+ {{ formError.power }} +
+
+ + + +
+
+ + +
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 new file mode 100644 index 0000000000..c61d3ef05d --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts @@ -0,0 +1,107 @@ +/* tslint:disable: member-ordering forin */ +// #docplaster +// #docregion +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; + +import { Hero } from '../shared/hero'; + +@Component({ + moduleId: module.id, + selector: 'hero-form-reactive', + 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'); + + submitted = false; + + onSubmit() { + this.submitted = true; + this.model = this.heroForm.value; + } +// #enddocregion class + + // 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, '', ''); + this.buildForm(); + this.onValueChanged(''); +// #enddocregion class + + this.active = false; + setTimeout(() => this.active = true, 0); +// #docregion class + } + + //// New with Reactive Form + + heroForm: FormGroup; + constructor(private builder: FormBuilder) { } + + ngOnInit(): void { this.buildForm(); } + + formError = { + '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.' + }, + '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 new file mode 100644 index 0000000000..dd9aecda74 --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts @@ -0,0 +1,13 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { SharedModule } from '../shared/shared.module'; +import { HeroFormReactiveComponent } from './hero-form-reactive.component'; + +@NgModule({ + imports: [ SharedModule, ReactiveFormsModule ], + declarations: [ HeroFormReactiveComponent ], + exports: [ HeroFormReactiveComponent ] +}) +export class HeroFormReactiveModule { } diff --git a/public/docs/_examples/cb-validation/ts/app/hero.ts b/public/docs/_examples/cb-form-validation/ts/app/shared/hero.ts similarity index 82% rename from public/docs/_examples/cb-validation/ts/app/hero.ts rename to public/docs/_examples/cb-form-validation/ts/app/shared/hero.ts index 3556a791c1..fe2b55e51a 100644 --- a/public/docs/_examples/cb-validation/ts/app/hero.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/shared/hero.ts @@ -1,13 +1,9 @@ -// #docplaster // #docregion export class Hero { - constructor( public id: number, public name: string, public power: string, public alterEgo?: string ) { } - } -// #enddocregion 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 new file mode 100644 index 0000000000..fd6c35ef2e --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/shared/shared.module.ts @@ -0,0 +1,12 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { SubmittedComponent } from './submitted.component'; + +@NgModule({ + imports: [ CommonModule], + declarations: [ SubmittedComponent ], + exports: [ CommonModule, SubmittedComponent ] +}) +export class SharedModule { } diff --git a/public/docs/_examples/cb-form-validation/ts/app/shared/submitted.component.ts b/public/docs/_examples/cb-form-validation/ts/app/shared/submitted.component.ts new file mode 100644 index 0000000000..18cea6563f --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/shared/submitted.component.ts @@ -0,0 +1,32 @@ +// #docregion +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { Hero } from './hero'; + +@Component({ + selector: 'hero-submitted', + template: ` +
+

You submitted the following:

+
+
Name
+
{{ hero.name }}
+
+
+
Alter Ego
+
{{ hero.alterEgo }}
+
+
+
Power
+
{{ hero.power }}
+
+
+ +
` +}) +export class SubmittedComponent { + @Input() hero: Hero; + @Input() submitted = false; + @Output() submittedChange = new EventEmitter(); + onClick() { this.submittedChange.emit(false); } +} diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.html b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.html similarity index 64% rename from public/docs/_examples/cb-validation/ts/app/hero-form-template.component.html rename to public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.html index e18f72c4c4..575036701e 100644 --- a/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.html +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.html @@ -1,19 +1,18 @@ -

Hero Form (Template-Driven)

-
- -
Name is required @@ -47,27 +46,12 @@
- - + +
-
-

You submitted the following:

-
-
Name
-
{{ model.name }}
-
-
-
Alter Ego
-
{{ model.alterEgo }}
-
-
-
Power
-
{{ model.power }}
-
-
- -
+
- diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.ts b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.ts similarity index 58% rename from public/docs/_examples/cb-validation/ts/app/hero-form-template.component.ts rename to public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.ts index d0a76ce246..9149ad3126 100644 --- a/public/docs/_examples/cb-validation/ts/app/hero-form-template.component.ts +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.component.ts @@ -1,38 +1,45 @@ +/* tslint:disable: member-ordering */ // #docplaster // #docregion import { Component } from '@angular/core'; -import { Hero } from './hero'; + +import { Hero } from '../shared/hero'; @Component({ + moduleId: module.id, selector: 'hero-form-template', - templateUrl: 'app/hero-form-template.component.html' + templateUrl: 'hero-form-template.component.html' }) // #docregion class export class HeroFormTemplateComponent { - powers = ['Really Smart', 'Super Flexible', - 'Super Hot', 'Weather Changer']; + powers = ['Really Smart', 'Super Flexible', 'Weather Changer']; + + model = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What'); - model = new Hero(18, 'Dr IQ', this.powers[0], - 'Chuck Overstreet'); -// #enddocregion class submitted = false; - onSubmit() { this.submitted = true; } + onSubmit() { + this.submitted = true; + } +// #enddocregion class // 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 class newHero() { this.model = new Hero(42, '', ''); +// #enddocregion class + this.active = false; - setTimeout(()=> this.active=true, 0); - } + setTimeout(() => this.active = true, 0); // #docregion class + } } // #enddocregion class // #enddocregion 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 new file mode 100644 index 0000000000..800ba19643 --- /dev/null +++ b/public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template.module.ts @@ -0,0 +1,13 @@ +// #docregion +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { SharedModule } from '../shared/shared.module'; +import { HeroFormTemplateComponent } from './hero-form-template.component'; + +@NgModule({ + imports: [ SharedModule, FormsModule ], + declarations: [ HeroFormTemplateComponent ], + exports: [ HeroFormTemplateComponent ] +}) +export class HeroFormTemplateModule { } diff --git a/public/docs/_examples/cb-validation/ts/example-config.json b/public/docs/_examples/cb-form-validation/ts/example-config.json similarity index 100% rename from public/docs/_examples/cb-validation/ts/example-config.json rename to public/docs/_examples/cb-form-validation/ts/example-config.json diff --git a/public/docs/_examples/cb-validation/ts/forms.css b/public/docs/_examples/cb-form-validation/ts/forms.css similarity index 100% rename from public/docs/_examples/cb-validation/ts/forms.css rename to public/docs/_examples/cb-form-validation/ts/forms.css diff --git a/public/docs/_examples/cb-validation/ts/index.html b/public/docs/_examples/cb-form-validation/ts/index.html similarity index 100% rename from public/docs/_examples/cb-validation/ts/index.html rename to public/docs/_examples/cb-form-validation/ts/index.html diff --git a/public/docs/_examples/cb-validation/ts/plnkr.json b/public/docs/_examples/cb-form-validation/ts/plnkr.json similarity index 100% rename from public/docs/_examples/cb-validation/ts/plnkr.json rename to public/docs/_examples/cb-form-validation/ts/plnkr.json diff --git a/public/docs/_examples/cb-validation/ts/app/app.module.ts b/public/docs/_examples/cb-validation/ts/app/app.module.ts deleted file mode 100644 index d20f416716..0000000000 --- a/public/docs/_examples/cb-validation/ts/app/app.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { FormsModule } from '@angular/forms'; -import { ReactiveFormsModule } from '@angular/forms'; - -import { AppComponent } from './app.component'; -import { HeroFormTemplateComponent } from './hero-form-template.component' -import { HeroFormModelComponent } from './hero-form-model.component' - -@NgModule({ - imports: [ - BrowserModule, - FormsModule, - ReactiveFormsModule - ], - declarations: [ - AppComponent, - HeroFormTemplateComponent, - HeroFormModelComponent - ], - bootstrap: [ AppComponent ] -}) -export class AppModule { } -// #enddocregion diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html b/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html deleted file mode 100644 index 9dc4b65635..0000000000 --- a/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.html +++ /dev/null @@ -1,61 +0,0 @@ - - -
-
-

Hero Form (Model-Driven)

-
-
- - - -
- {{ formError.name }} -
- -
- -
- - -
- -
- - -
- {{ formError.power }} -
-
- - - -
-
- -
-

You submitted the following:

-
-
Name
-
{{ model.name }}
-
-
-
Alter Ego
-
{{ model.alterEgo }}
-
-
-
Power
-
{{ model.power }}
-
-
- -
-
- diff --git a/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts b/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts deleted file mode 100644 index 11037c7dd3..0000000000 --- a/public/docs/_examples/cb-validation/ts/app/hero-form-model.component.ts +++ /dev/null @@ -1,107 +0,0 @@ -// #docplaster -// #docregion -import { Component, OnInit } from '@angular/core'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - -import { Hero } from './hero'; - -@Component({ - selector: 'hero-form-model', - templateUrl: 'app/hero-form-model.component.html' -}) -// #docregion class -export class HeroFormModelComponent implements OnInit { - heroForm: FormGroup; - formError: { [id: string]: string }; - private validationMessages: { [id: string]: { [id: string]: string } }; - - powers = ['Really Smart', 'Super Flexible', - 'Super Hot', 'Weather Changer']; - - model = new Hero(18, 'Dr IQ', this.powers[0], - 'Chuck Overstreet'); - -// #enddocregion class - submitted = false; -// #docregion class - constructor(private fb: FormBuilder) { - this.formError = { - 'name': '', - 'alterEgo': '', - 'power': '' - }; - this.validationMessages = { - 'name': { - 'required': 'Name is required.', - 'minlength': 'Name must be at least 4 characters long.', - 'maxlength': 'Name cannot be more than 24 characters long.' - }, - 'power': { - 'required': 'Power is required.' - } - }; - } - - ngOnInit(): void { - this.buildForm(); - } - - buildForm(): void { - this.heroForm = this.fb.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) { - for (let field in this.formError) { - if (this.formError.hasOwnProperty(field)) { - let hasError = (this.heroForm.controls[field].dirty) && - !this.heroForm.controls[field].valid; - this.formError[field] = ''; - if (hasError) { - for (let key in this.heroForm.controls[field].errors) { - if (this.heroForm.controls[field].errors.hasOwnProperty(key)) { - this.formError[field] += this.validationMessages[field][key] + ' '; - } - } - } - } - } - } -// #enddocregion class - onSubmit() { - this.submitted = true; - this.model = this.heroForm.value; - } - - isRequired(controlName: string): boolean { - if (Object.keys(this.validationMessages).includes(controlName)) { - return Object.keys(this.validationMessages[controlName]).includes('required');} - return false; - } - // 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; - - newHero() { - this.model = new Hero(42, '', ''); - this.buildForm(); - this.onValueChanged(''); - this.active = false; - setTimeout(() => this.active = true, 0); - } - // #docregion class -} -// #enddocregion class -// #enddocregion diff --git a/public/docs/dart/latest/cookbook/_data.json b/public/docs/dart/latest/cookbook/_data.json index 9ea1a5f8bf..931857b846 100644 --- a/public/docs/dart/latest/cookbook/_data.json +++ b/public/docs/dart/latest/cookbook/_data.json @@ -41,6 +41,12 @@ "hide": true }, + "form-validation": { + "title": "Form Validation", + "intro": "Validate user's form entries", + "hide": true + }, + "rc4-to-rc5": { "title": "RC4 to RC5 Migration", "intro": "Migrate your RC4 app to RC5 in minutes.", diff --git a/public/docs/dart/latest/cookbook/dynamic-form.jade b/public/docs/dart/latest/cookbook/dynamic-form.jade index f8df2a84a6..6778b6af28 100644 --- a/public/docs/dart/latest/cookbook/dynamic-form.jade +++ b/public/docs/dart/latest/cookbook/dynamic-form.jade @@ -1 +1 @@ -!= partial("../../../_includes/_ts-temp") \ No newline at end of file +!= partial("../../../_includes/_ts-temp") diff --git a/public/docs/dart/latest/cookbook/form-validation.jade b/public/docs/dart/latest/cookbook/form-validation.jade new file mode 100644 index 0000000000..6778b6af28 --- /dev/null +++ b/public/docs/dart/latest/cookbook/form-validation.jade @@ -0,0 +1 @@ +!= partial("../../../_includes/_ts-temp") diff --git a/public/docs/js/latest/cookbook/_data.json b/public/docs/js/latest/cookbook/_data.json index 9a43659334..7443923155 100644 --- a/public/docs/js/latest/cookbook/_data.json +++ b/public/docs/js/latest/cookbook/_data.json @@ -37,6 +37,11 @@ "intro": "Render dynamic forms with NgFormModel" }, + "form-validation": { + "title": "Form Validation", + "intro": "Validate user's form entries" + }, + "rc4-to-rc5": { "title": "RC4 to RC5 Migration", "intro": "Migrate your RC4 app to RC5 in minutes.", diff --git a/public/docs/js/latest/cookbook/form-validation.jade b/public/docs/js/latest/cookbook/form-validation.jade new file mode 100644 index 0000000000..6778b6af28 --- /dev/null +++ b/public/docs/js/latest/cookbook/form-validation.jade @@ -0,0 +1 @@ +!= partial("../../../_includes/_ts-temp") diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index 4cc4187bc7..6142c4c6f4 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -43,16 +43,16 @@ "intro": "Render dynamic forms with FormGroup" }, + "form-validation": { + "title": "Form Validation", + "intro": "Validate user's form entries" + }, + "rc4-to-rc5": { "title": "RC4 to RC5 Migration", "intro": "Migrate your RC4 app to RC5 in minutes." }, - "validation": { - "title": "Validation", - "intro": "Validate user's form entries" - }, - "set-document-title": { "title": "Set the Document Title", "intro": "Setting the document or window title using the Title service." diff --git a/public/docs/ts/latest/cookbook/validation.jade b/public/docs/ts/latest/cookbook/form-validation.jade similarity index 56% rename from public/docs/ts/latest/cookbook/validation.jade rename to public/docs/ts/latest/cookbook/form-validation.jade index 4cccdbbb13..ba0e9693de 100644 --- a/public/docs/ts/latest/cookbook/validation.jade +++ b/public/docs/ts/latest/cookbook/form-validation.jade @@ -2,91 +2,97 @@ include ../_util-fns :marked - We want our data to be accurate and complete. By helping the user enter - appropriate data and confirming that data is valid, we can improve the quality of that incoming data. - - In this cookbook we show how to validate data and display useful validation messages using first the - template-driven forms and then the reactive forms approach. + We can improve overall data quality by validating user input for accuracy and completeness. - An Angular component is comprised of a template and a component class containing the code that drives the template. - The first example demonstrates how to validate data using only the template. The second technique - moves the validation logic out of the template and into the component class, giving you more control and better unit testing. + 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. + + 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. - In these examples we use the form created in the [Forms chapter.](../guide/forms.html) + Both examples are based on the the sample form in the [Forms chapter.](../guide/forms.html) :marked - ## Table of contents + ## Contents - [Template-Driven Forms Approach](#template-driven) + [Template-Driven Forms Approach](#template-driven) - [Reactive Forms Approach](#model-driven) + [Reactive Forms Approach](#reactive) -:marked - **See the [live example](/resources/live-examples/cb-validation/ts/plnkr.html)**. + **Try the live example** + .l-main-section :marked - ## Template-Driven Forms Approach + ## Template-Driven Forms - Using the template-driven approach to form validation, the validation is defined in the template. - Each control on the form defines its validation, binding, and validation messages in the template. + In the template-driven approach, + each control on the form defines its own validation and validation messages in the template. -+makeExample('cb-validation/ts/app/hero-form-template.component.html','name-with-error-msg','app/hero-form-template.component.html') + 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)') :marked - Here we define a standard label and set up an input box for validation of the hero name as follows: - - Add the desired HTML validation attributes. In this example, - we add `required`, `minlength`, and `maxlength` attributes on the input box to define the validation rules. - - Set the `name` attribute of the input box. This is required for Angular to track this input element. - - Use `ngModel` binding to set the default value and track the user's changes to the input box. - This registers the input box as a control that is associated with the `ngForm` directive. - - Define a template reference variable (`name` in this example) that references the registered control. - We use this variable to reference this control when checking the control state, such as `valid` or `dirty`. + Note the following: + - The `` element implements the validation rules as HTML validation attributes: `required`, `minlength`, and `maxlength`. - Next we define a `div` element for the validation error messages. - We use the template reference variable to determine whether to display one of the validation messages. - - We use `*ngIf` to check whether the control has errors and whether it is `dirty` or `touched` before - displaying the validation block `div`. - - We then have a separate `div` for each possible validation error. In our example, we defined validation for - `required`, `minlength`, and `maxlength`, so we add one `div` for each one. Each `div` is marked with `[hidden]` - so the validation message is hidden unless the control has the specified error. + - Set the `name` attribute of the input box so Angular can track this input element. - Repeat for each data entry control on the form. + - 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. + + - 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`. + + - A `
` element for a group of validation error messages. + The `*ngIf` reveals the error group if there are any 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`. + + The full template repeats this kind of layout for each data entry control on the form. .l-sub-section :marked - Adding the check for `dirty` or `touched` prevents display of errors before the user has a chance to edit the - value. This is most useful when adding new data, such as a new hero. + 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 model used in the data binding. And provides any other code required by 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-validation/ts/app/hero-form-template.component.ts','class','app/hero-form-template.component.ts') ++makeExample('cb-form-validation/ts/app/template/hero-form-template.component.ts','class','app/template/hero-form-template.component.ts') :marked Use this template-driven validation technique when working with simple forms with simple validation scenarios. - Here's the complete solution for the template-driven approach: + Here are the pertinent files for the template-driven approach: +makeTabs( - `cb-validation/ts/app/main.ts, - cb-validation/ts/app/app.module.ts, - cb-validation/ts/app/app.component.ts, - cb-validation/ts/app/hero.ts, - cb-validation/ts/app/hero-form-template.component.html, - cb-validation/ts/app/hero-form-template.component.ts`, + `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`, '', - 'app/main.ts, app/app.module.ts, app/app.component.ts, app/hero.ts, app/hero-form-template.component.html, app/hero-form-template.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`) .l-main-section - + :marked - ## Reactive Forms Approach + ## Reactive Forms - The alternate way to implement forms in Angular is to use reactive forms, previously called `model-driven forms`. - Using the reactive forms approach to form validation, the validation rules are specified in the model as + 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. @@ -100,7 +106,7 @@ include ../_util-fns When moving the validation attributes out of the HTML, we are no longer aria ready. Work is being done to address this. -+makeExample('cb-validation/ts/app/hero-form-model.component.ts','class','app/hero-form-model.component.ts') ++makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts','class','app/reactive/hero-form-reactive.component.ts') :marked In the component's class, we define the form and our own data structures to manage definition and display of the validation messages: @@ -153,7 +159,7 @@ include ../_util-fns 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-validation/ts/app/hero-form-model.component.html','name-with-error-msg','app/hero-form-model.component.html') ++makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.html','name-with-error-msg','app/reactive/hero-form-reactive.component.html') :marked In the template, define a standard label and set up an input box for validation as follows: @@ -173,17 +179,20 @@ include ../_util-fns Use this technique when you want better control over the validation rules and messsages. - Here's the complete solution for the reactive forms approach: + Here are the pertinent files for the reactive forms approach: +makeTabs( - `cb-validation/ts/app/main.ts, - cb-validation/ts/app/app.module.ts, - cb-validation/ts/app/app.component.ts, - cb-validation/ts/app/hero.ts, - cb-validation/ts/app/hero-form-model.component.html, - cb-validation/ts/app/hero-form-model.component.ts`, + `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/main.ts, app/app.module.ts, app/app.component.ts, app/hero.ts, app/hero-form-model.component.html, app/hero-form-model.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`) :marked - [Back to top](#top) \ No newline at end of file + [Back to top](#top) diff --git a/public/resources/images/cookbooks/form-validation/plunker.png b/public/resources/images/cookbooks/form-validation/plunker.png new file mode 100644 index 0000000000000000000000000000000000000000..0df5840a525f41345792e99f36555a51a0f20e49 GIT binary patch literal 25379 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

8gt82N#X9R0W^H5H%pOUD7ESf1hYMw7a< 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 tw Date: Sun, 28 Aug 2016 17:07:13 -0700 Subject: [PATCH 3/3] docs(cb-form-validation): add template2 - a step between template and reactive Raises questions about what really separates Forms from ReactiveForms --- .../_examples/cb-form-validation/e2e-spec.ts | 1 + .../ts/app/app.component.ts | 6 +- .../hero-form-reactive.component.html | 30 +- .../reactive/hero-form-reactive.component.ts | 122 ++-- .../app/reactive/hero-form-reactive.module.ts | 2 +- .../ts/app/shared/forbidden-name.directive.ts | 43 ++ .../ts/app/shared/shared.module.ts | 12 +- .../app/template/hero-form-template.module.ts | 9 +- ...tml => hero-form-template1.component.html} | 42 +- ...nt.ts => hero-form-template1.component.ts} | 17 +- .../hero-form-template2.component.html | 52 ++ .../template/hero-form-template2.component.ts | 99 ++++ .../cb-form-validation/ts/index.html | 2 +- .../ts/latest/cookbook/form-validation.jade | 534 +++++++++++++----- .../cookbooks/form-validation/plunker.png | Bin 25379 -> 21082 bytes 15 files changed, 726 insertions(+), 245 deletions(-) create mode 100644 public/docs/_examples/cb-form-validation/ts/app/shared/forbidden-name.directive.ts rename public/docs/_examples/cb-form-validation/ts/app/template/{hero-form-template.component.html => hero-form-template1.component.html} (60%) rename public/docs/_examples/cb-form-validation/ts/app/template/{hero-form-template.component.ts => hero-form-template1.component.ts} (71%) create mode 100644 public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.html create mode 100644 public/docs/_examples/cb-form-validation/ts/app/template/hero-form-template2.component.ts 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 0df5840a525f41345792e99f36555a51a0f20e49..c1fd3eed096b8681e9b619a2de3474c07e76470b 100644 GIT binary patch literal 21082 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 K+T~hyq5lQ%I+4@> literal 25379 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

8gt82N#X9R0W^H5H%pOUD7ESf1hYMw7a< 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 tw