docs(cb-form-validation): create Form Validation Cookbook

This commit is contained in:
Deborah Kurata 2016-06-01 11:54:30 -07:00 committed by Ward Bell
parent f49b611231
commit 3b4a5d533d
15 changed files with 641 additions and 0 deletions

View File

@ -0,0 +1,63 @@
/// <reference path="../_protractor/e2e.d.ts" />
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);
});
});
});

View File

@ -0,0 +1,12 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `<hero-form-template></hero-form-template>
<hr>
<hero-form-model></hero-form-model>`
})
export class AppComponent { }
// #enddocregion

View File

@ -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

View File

@ -0,0 +1,61 @@
<!-- #docplaster -->
<!-- #docregion -->
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form (Model-Driven)</h1>
<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name3">Name</label>
<input type="text" id="name3" class="form-control"
formControlName="name"
[ngClass]="{'required': isRequired('name')}">
<div *ngIf="formError.name" class="alert alert-danger">
{{ formError.name }}
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo3">Alter Ego</label>
<input type="text" id="alterEgo3" class="form-control"
formControlName="alterEgo"
[ngClass]="{'required': isRequired('alterEgo')}" >
</div>
<div class="form-group">
<label for="power3">Hero Power</label>
<select id="power3" class="form-control"
formControlName="power"
[ngClass]="{'required': isRequired('power')}" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="formError.power" class="alert alert-danger">
{{ formError.power }}
</div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!heroForm.valid">Submit</button>
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
</form>
</div>
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9 pull-left">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9 pull-left">{{ model.power }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">Edit</button>
</div>
</div>
<!-- #enddocregion -->

View File

@ -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

View File

@ -0,0 +1,73 @@
<!-- #docplaster -->
<!-- #docregion -->
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form (Template-Driven)</h1>
<form *ngIf="active"
(ngSubmit)="onSubmit()"
#heroForm="ngForm">
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label>
<input type="text" id="name" class="form-control"
required minlength="4" maxlength="24"
name="name" [(ngModel)]="model.name"
#name="ngModel" >
<div *ngIf="name.errors && (name.dirty || name.touched)"
class="alert alert-danger">
<div [hidden]="!name.errors.required">
Name is required
</div>
<div [hidden]="!name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div [hidden]="!name.errors.maxlength">
Name cannot be more than 24 characters long.
</div>
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" id="alterEgo" class="form-control"
name="alterEgo" [(ngModel)]="model.alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select id="power" class="form-control"
required
name="power" [(ngModel)]="model.power"
#power="ngModel" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="power.errors && power.touched" class="alert alert-danger">
<div [hidden]="!power.errors.required">Power is required</div>
</div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
</form>
</div>
<div [hidden]="!submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9 pull-left">{{ model.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9 pull-left">{{ model.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9 pull-left">{{ model.power }}</div>
</div>
<br>
<button class="btn btn-default" (click)="submitted=false">Edit</button>
</div>
</div>
<!-- #enddocregion -->

View File

@ -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

View File

@ -0,0 +1,13 @@
// #docplaster
// #docregion
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}
// #enddocregion

View File

@ -0,0 +1,8 @@
// #docplaster
// #docregion
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
// #enddocregion

View File

@ -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 */
}

View File

@ -0,0 +1,29 @@
<html>
<head>
<title>Hero Form with Validation</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="forms.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>

View File

@ -0,0 +1,7 @@
{
"description": "Validation",
"files":[
"!**/*.d.ts",
"!**/*.js"
]
}

View File

@ -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."

View File

@ -0,0 +1,189 @@
include ../_util-fns
<a id="top"></a>
: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)
<a id="toc"></a>
: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
<a id="template-driven"></a>
: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
<a id="model-driven"></a>
: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)