docs(cb-form-validation): create Form Validation Cookbook
This commit is contained in:
parent
f49b611231
commit
3b4a5d533d
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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 -->
|
|
@ -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
|
|
@ -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 -->
|
|
@ -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
|
|
@ -0,0 +1,13 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
export class Hero {
|
||||
|
||||
constructor(
|
||||
public id: number,
|
||||
public name: string,
|
||||
public power: string,
|
||||
public alterEgo?: string
|
||||
) { }
|
||||
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,8 @@
|
|||
// #docplaster
|
||||
// #docregion
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
// #enddocregion
|
|
@ -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 */
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"description": "Validation",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js"
|
||||
]
|
||||
}
|
|
@ -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."
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue