From 3b4a5d533d4c9a0595bbdab676382ebe23a89fce Mon Sep 17 00:00:00 2001 From: Deborah Kurata Date: Wed, 1 Jun 2016 11:54:30 -0700 Subject: [PATCH] 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