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)
+
+
+
+
+
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)
+
+
+
+
+
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