Skip to content
This repository was archived by the owner on Dec 4, 2017. It is now read-only.

Commit b2ce694

Browse files
authored
docs(cb): Add cookbook for validation (#1627)
* docs(cb-form-validation): create Form Validation Cookbook * docs(cb-form-validation): ward's tweaks renamed from cb-validation to cb-form-validation other refactorings and text changes * docs(cb-form-validation): add template2 - a step between template and reactive Raises questions about what really separates Forms from ReactiveForms
2 parents f49b611 + 1212b51 commit b2ce694

28 files changed

+1175
-1
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/// <reference path="../_protractor/e2e.d.ts" />
2+
'use strict'; // necessary for node!
3+
describeIf(browser.appIsTs || browser.appIsJs, 'Forms Tests', function () {
4+
5+
beforeEach(function () {
6+
browser.get('');
7+
});
8+
9+
it('should display correct title', function () {
10+
expect(element.all(by.css('h1')).get(0).getText()).toEqual('Hero Form');
11+
});
12+
13+
14+
it('should not display message before submit', function () {
15+
let ele = element(by.css('h2'));
16+
expect(ele.isDisplayed()).toBe(false);
17+
});
18+
19+
it('should hide form after submit', function () {
20+
let ele = element.all(by.css('h1')).get(0);
21+
expect(ele.isDisplayed()).toBe(true);
22+
let b = element.all(by.css('button[type=submit]')).get(0);
23+
b.click().then(function() {
24+
expect(ele.isDisplayed()).toBe(false);
25+
});
26+
});
27+
28+
it('should display message after submit', function () {
29+
let b = element.all(by.css('button[type=submit]')).get(0);
30+
b.click().then(function() {
31+
expect(element(by.css('h2')).getText()).toContain('You submitted the following');
32+
});
33+
});
34+
35+
it('should hide form after submit', function () {
36+
let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0);
37+
expect(alterEgoEle.isDisplayed()).toBe(true);
38+
let submitButtonEle = element.all(by.css('button[type=submit]')).get(0);
39+
submitButtonEle.click().then(function() {
40+
expect(alterEgoEle.isDisplayed()).toBe(false);
41+
});
42+
});
43+
44+
it('should reflect submitted data after submit', function () {
45+
let test = 'testing 1 2 3';
46+
let newValue: string;
47+
let alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0);
48+
alterEgoEle.getAttribute('value').then(function(value) {
49+
// alterEgoEle.sendKeys(test);
50+
sendKeys(alterEgoEle, test);
51+
newValue = value + test;
52+
expect(alterEgoEle.getAttribute('value')).toEqual(newValue);
53+
}).then(function() {
54+
let b = element.all(by.css('button[type=submit]')).get(0);
55+
return b.click();
56+
}).then(function() {
57+
let alterEgoTextEle = element(by.cssContainingText('div', 'Alter Ego'));
58+
expect(alterEgoTextEle.isPresent()).toBe(true, 'cannot locate "Alter Ego" label');
59+
let divEle = element(by.cssContainingText('div', newValue));
60+
expect(divEle.isPresent()).toBe(true, 'cannot locate div with this text: ' + newValue);
61+
});
62+
});
63+
});
64+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// #docregion
2+
import { Component } from '@angular/core';
3+
4+
@Component({
5+
selector: 'my-app',
6+
template: `<hero-form-template1></hero-form-template1>
7+
<hr>
8+
<hero-form-template2></hero-form-template2>
9+
<hr>
10+
<hero-form-reactive3></hero-form-reactive3>`
11+
})
12+
export class AppComponent { }
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// #docregion
2+
import { NgModule } from '@angular/core';
3+
import { BrowserModule } from '@angular/platform-browser';
4+
5+
import { AppComponent } from './app.component';
6+
import { HeroFormTemplateModule } from './template/hero-form-template.module';
7+
import { HeroFormReactiveModule } from './reactive/hero-form-reactive.module';
8+
9+
@NgModule({
10+
imports: [
11+
BrowserModule,
12+
HeroFormTemplateModule,
13+
HeroFormReactiveModule
14+
],
15+
declarations: [ AppComponent ],
16+
bootstrap: [ AppComponent ]
17+
})
18+
export class AppModule { }
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// #docregion
2+
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3+
4+
import { AppModule } from './app.module';
5+
6+
platformBrowserDynamic().bootstrapModule(AppModule);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<!-- #docregion -->
2+
<div class="container">
3+
<div [hidden]="submitted">
4+
<h1>Hero Form 3 (Reactive)</h1>
5+
<!-- #docregion form-tag-->
6+
<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">
7+
<!-- #enddocregion form-tag-->
8+
<div class="form-group">
9+
<!-- #docregion name-with-error-msg -->
10+
<label for="name">Name</label>
11+
12+
<input type="text" id="name" class="form-control"
13+
formControlName="name" required >
14+
15+
<div *ngIf="formErrors.name" class="alert alert-danger">
16+
{{ formErrors.name }}
17+
</div>
18+
<!-- #enddocregion name-with-error-msg -->
19+
</div>
20+
21+
<div class="form-group">
22+
<label for="alterEgo">Alter Ego</label>
23+
<input type="text" id="alterEgo" class="form-control"
24+
formControlName="alterEgo" >
25+
</div>
26+
27+
<div class="form-group">
28+
<label for="power">Hero Power</label>
29+
<select id="power" class="form-control"
30+
formControlName="power" required >
31+
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
32+
</select>
33+
34+
<div *ngIf="formErrors.power" class="alert alert-danger">
35+
{{ formErrors.power }}
36+
</div>
37+
</div>
38+
39+
<button type="submit" class="btn btn-default"
40+
[disabled]="!heroForm.valid">Submit</button>
41+
<button type="button" class="btn btn-default"
42+
(click)="addHero()">New Hero</button>
43+
</form>
44+
</div>
45+
46+
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
47+
</div>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/* tslint:disable: member-ordering forin */
2+
// #docplaster
3+
// #docregion
4+
import { Component, OnInit } from '@angular/core';
5+
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
6+
7+
import { Hero } from '../shared/hero';
8+
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
9+
10+
@Component({
11+
moduleId: module.id,
12+
selector: 'hero-form-reactive3',
13+
templateUrl: 'hero-form-reactive.component.html'
14+
})
15+
export class HeroFormReactiveComponent implements OnInit {
16+
17+
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
18+
19+
hero = new Hero(18, 'Dr. WhatIsHisName', this.powers[0], 'Dr. What');
20+
21+
submitted = false;
22+
23+
// #docregion on-submit
24+
onSubmit() {
25+
this.submitted = true;
26+
this.hero = this.heroForm.value;
27+
}
28+
// #enddocregion on-submit
29+
// #enddocregion
30+
31+
// Reset the form with a new hero AND restore 'pristine' class state
32+
// by toggling 'active' flag which causes the form
33+
// to be removed/re-added in a tick via NgIf
34+
// TODO: Workaround until NgForm has a reset method (#6822)
35+
active = true;
36+
// #docregion
37+
// #docregion add-hero
38+
addHero() {
39+
this.hero = new Hero(42, '', '');
40+
this.buildForm();
41+
this.onValueChanged();
42+
// #enddocregion add-hero
43+
// #enddocregion class
44+
45+
this.active = false;
46+
setTimeout(() => this.active = true, 0);
47+
// #docregion
48+
// #docregion add-hero
49+
}
50+
// #enddocregion add-hero
51+
52+
// #docregion form-builder
53+
heroForm: FormGroup;
54+
constructor(private fb: FormBuilder) { }
55+
56+
ngOnInit(): void {
57+
this.buildForm();
58+
}
59+
60+
buildForm(): void {
61+
this.heroForm = this.fb.group({
62+
// #docregion name-validators
63+
'name': [this.hero.name, [
64+
Validators.required,
65+
Validators.minLength(4),
66+
Validators.maxLength(24),
67+
forbiddenNameValidator(/bob/i)
68+
]
69+
],
70+
// #enddocregion name-validators
71+
'alterEgo': [this.hero.alterEgo],
72+
'power': [this.hero.power, Validators.required]
73+
});
74+
75+
this.heroForm.valueChanges
76+
.subscribe(data => this.onValueChanged(data));
77+
}
78+
79+
// #enddocregion form-builder
80+
81+
onValueChanged(data?: any) {
82+
const controls = this.heroForm ? this.heroForm.controls : {};
83+
84+
for (const field in this.formErrors) {
85+
// clear previous error message (if any)
86+
this.formErrors[field] = '';
87+
const control = controls[field];
88+
89+
if (control && control.dirty && !control.valid) {
90+
const messages = this.validationMessages[field];
91+
for (const key in control.errors) {
92+
this.formErrors[field] += messages[key] + ' ';
93+
}
94+
}
95+
}
96+
}
97+
98+
formErrors = {
99+
'name': '',
100+
'power': ''
101+
};
102+
103+
validationMessages = {
104+
'name': {
105+
'required': 'Name is required.',
106+
'minlength': 'Name must be at least 4 characters long.',
107+
'maxlength': 'Name cannot be more than 24 characters long.',
108+
'forbiddenName': 'Someone named "Bob" cannot be a hero.'
109+
},
110+
'power': {
111+
'required': 'Power is required.'
112+
}
113+
};
114+
}
115+
// #enddocregion
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// #docregion
2+
import { NgModule } from '@angular/core';
3+
import { ReactiveFormsModule } from '@angular/forms';
4+
5+
import { SharedModule } from '../shared/shared.module';
6+
import { HeroFormReactiveComponent } from './hero-form-reactive.component';
7+
8+
@NgModule({
9+
imports: [ SharedModule, ReactiveFormsModule ],
10+
declarations: [ HeroFormReactiveComponent ],
11+
exports: [ HeroFormReactiveComponent ]
12+
})
13+
export class HeroFormReactiveModule { }
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// #docregion
2+
import { Directive, Input, OnChanges, SimpleChanges } from '@angular/core';
3+
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn, Validators } from '@angular/forms';
4+
5+
// #docregion custom-validator
6+
/** A hero's name can't match the given regular expression */
7+
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
8+
return (control: AbstractControl): {[key: string]: any} => {
9+
const name = control.value;
10+
const no = nameRe.test(name);
11+
return no ? {'forbiddenName': {name}} : null;
12+
};
13+
}
14+
// #enddocregion custom-validator
15+
16+
// #docregion directive
17+
@Directive({
18+
selector: '[forbiddenName]',
19+
// #docregion directive-providers
20+
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
21+
// #enddocregion directive-providers
22+
})
23+
export class ForbiddenValidatorDirective implements Validator, OnChanges {
24+
@Input() forbiddenName: string;
25+
private valFn = Validators.nullValidator;
26+
27+
ngOnChanges(changes: SimpleChanges): void {
28+
const change = changes['forbiddenName'];
29+
if (change) {
30+
const val: string | RegExp = change.currentValue;
31+
const re = val instanceof RegExp ? val : new RegExp(val, 'i');
32+
this.valFn = forbiddenNameValidator(re);
33+
} else {
34+
this.valFn = Validators.nullValidator;
35+
}
36+
}
37+
38+
validate(control: AbstractControl): {[key: string]: any} {
39+
return this.valFn(control);
40+
}
41+
}
42+
// #docregion directive
43+
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// #docregion
2+
export class Hero {
3+
constructor(
4+
public id: number,
5+
public name: string,
6+
public power: string,
7+
public alterEgo?: string
8+
) { }
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// #docregion
2+
import { NgModule } from '@angular/core';
3+
import { CommonModule } from '@angular/common';
4+
5+
import { ForbiddenValidatorDirective } from './forbidden-name.directive';
6+
import { SubmittedComponent } from './submitted.component';
7+
8+
@NgModule({
9+
imports: [ CommonModule],
10+
declarations: [ ForbiddenValidatorDirective, SubmittedComponent ],
11+
exports: [ ForbiddenValidatorDirective, SubmittedComponent,
12+
CommonModule ]
13+
})
14+
export class SharedModule { }
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// #docregion
2+
import { Component, EventEmitter, Input, Output } from '@angular/core';
3+
4+
import { Hero } from './hero';
5+
6+
@Component({
7+
selector: 'hero-submitted',
8+
template: `
9+
<div *ngIf="submitted">
10+
<h2>You submitted the following:</h2>
11+
<div class="row">
12+
<div class="col-xs-3">Name</div>
13+
<div class="col-xs-9 pull-left">{{ hero.name }}</div>
14+
</div>
15+
<div class="row">
16+
<div class="col-xs-3">Alter Ego</div>
17+
<div class="col-xs-9 pull-left">{{ hero.alterEgo }}</div>
18+
</div>
19+
<div class="row">
20+
<div class="col-xs-3">Power</div>
21+
<div class="col-xs-9 pull-left">{{ hero.power }}</div>
22+
</div>
23+
<br>
24+
<button class="btn btn-default" (click)="onClick()">Edit</button>
25+
</div>`
26+
})
27+
export class SubmittedComponent {
28+
@Input() hero: Hero;
29+
@Input() submitted = false;
30+
@Output() submittedChange = new EventEmitter<boolean>();
31+
onClick() { this.submittedChange.emit(false); }
32+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// #docregion
2+
import { NgModule } from '@angular/core';
3+
import { FormsModule } from '@angular/forms';
4+
5+
import { SharedModule } from '../shared/shared.module';
6+
import { HeroFormTemplate1Component } from './hero-form-template1.component';
7+
import { HeroFormTemplate2Component } from './hero-form-template2.component';
8+
9+
@NgModule({
10+
imports: [ SharedModule, FormsModule ],
11+
declarations: [ HeroFormTemplate1Component, HeroFormTemplate2Component ],
12+
exports: [ HeroFormTemplate1Component, HeroFormTemplate2Component ]
13+
})
14+
export class HeroFormTemplateModule { }

0 commit comments

Comments
 (0)