docs(cb-form-validation): ward's tweaks
renamed from cb-validation to cb-form-validation other refactorings and text changes
This commit is contained in:
parent
3b4a5d533d
commit
f971685a7c
|
@ -1,4 +1,3 @@
|
||||||
// #docplaster
|
|
||||||
// #docregion
|
// #docregion
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@ -6,7 +5,6 @@ import { Component } from '@angular/core';
|
||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
template: `<hero-form-template></hero-form-template>
|
template: `<hero-form-template></hero-form-template>
|
||||||
<hr>
|
<hr>
|
||||||
<hero-form-model></hero-form-model>`
|
<hero-form-reactive></hero-form-reactive>`
|
||||||
})
|
})
|
||||||
export class AppComponent { }
|
export class AppComponent { }
|
||||||
// #enddocregion
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// #docregion
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { HeroFormTemplateModule } from './template/hero-form-template.module';
|
||||||
|
import { HeroFormReactiveModule } from './reactive/hero-form-reactive.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
HeroFormTemplateModule,
|
||||||
|
HeroFormReactiveModule
|
||||||
|
],
|
||||||
|
declarations: [ AppComponent ],
|
||||||
|
bootstrap: [ AppComponent ]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
|
@ -1,8 +1,6 @@
|
||||||
// #docplaster
|
|
||||||
// #docregion
|
// #docregion
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
platformBrowserDynamic().bootstrapModule(AppModule);
|
||||||
// #enddocregion
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!-- #docregion -->
|
||||||
|
<div class="container">
|
||||||
|
<div [hidden]="submitted">
|
||||||
|
<h1>Hero Form (Reactive)</h1>
|
||||||
|
<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">
|
||||||
|
<div class="form-group">
|
||||||
|
<!-- #docregion name-with-error-msg -->
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" id="name" 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="alterEgo">Alter Ego</label>
|
||||||
|
<input type="text" id="alterEgo" class="form-control"
|
||||||
|
formControlName="alterEgo"
|
||||||
|
[ngClass]="{'required': isRequired('alterEgo')}" >
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="power">Hero Power</label>
|
||||||
|
<select id="power" 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>
|
||||||
|
|
||||||
|
<hero-submitted [hero]="model" [(submitted)]="submitted"></hero-submitted>
|
||||||
|
</div>
|
|
@ -0,0 +1,107 @@
|
||||||
|
/* tslint:disable: member-ordering forin */
|
||||||
|
// #docplaster
|
||||||
|
// #docregion
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
import { Hero } from '../shared/hero';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
moduleId: module.id,
|
||||||
|
selector: 'hero-form-reactive',
|
||||||
|
templateUrl: 'hero-form-reactive.component.html'
|
||||||
|
})
|
||||||
|
// #docregion class
|
||||||
|
export class HeroFormReactiveComponent implements OnInit {
|
||||||
|
|
||||||
|
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
||||||
|
|
||||||
|
model = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
|
||||||
|
|
||||||
|
submitted = false;
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
this.submitted = true;
|
||||||
|
this.model = this.heroForm.value;
|
||||||
|
}
|
||||||
|
// #enddocregion class
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// #docregion class
|
||||||
|
newHero() {
|
||||||
|
this.model = new Hero(42, '', '');
|
||||||
|
this.buildForm();
|
||||||
|
this.onValueChanged('');
|
||||||
|
// #enddocregion class
|
||||||
|
|
||||||
|
this.active = false;
|
||||||
|
setTimeout(() => this.active = true, 0);
|
||||||
|
// #docregion class
|
||||||
|
}
|
||||||
|
|
||||||
|
//// New with Reactive Form
|
||||||
|
|
||||||
|
heroForm: FormGroup;
|
||||||
|
constructor(private builder: FormBuilder) { }
|
||||||
|
|
||||||
|
ngOnInit(): void { this.buildForm(); }
|
||||||
|
|
||||||
|
formError = {
|
||||||
|
'name': '',
|
||||||
|
'power': ''
|
||||||
|
};
|
||||||
|
|
||||||
|
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.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
buildForm(): void {
|
||||||
|
this.heroForm = this.builder.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) {
|
||||||
|
const controls = this.heroForm ? this.heroForm.controls : {};
|
||||||
|
for (const field in this.formError) {
|
||||||
|
// clear previous error message (if any)
|
||||||
|
this.formError[field] = '';
|
||||||
|
const control = controls[field];
|
||||||
|
if (control && control.dirty && !control.valid) {
|
||||||
|
const messages = this.validationMessages[field];
|
||||||
|
for (const key in control.errors) {
|
||||||
|
this.formError[field] += messages[key] + ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRequired(controlName: string): boolean {
|
||||||
|
const msgs = this.validationMessages[controlName];
|
||||||
|
return msgs && msgs['required'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #enddocregion class
|
||||||
|
// #enddocregion
|
|
@ -0,0 +1,13 @@
|
||||||
|
// #docregion
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { HeroFormReactiveComponent } from './hero-form-reactive.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ SharedModule, ReactiveFormsModule ],
|
||||||
|
declarations: [ HeroFormReactiveComponent ],
|
||||||
|
exports: [ HeroFormReactiveComponent ]
|
||||||
|
})
|
||||||
|
export class HeroFormReactiveModule { }
|
|
@ -1,13 +1,9 @@
|
||||||
// #docplaster
|
|
||||||
// #docregion
|
// #docregion
|
||||||
export class Hero {
|
export class Hero {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public id: number,
|
public id: number,
|
||||||
public name: string,
|
public name: string,
|
||||||
public power: string,
|
public power: string,
|
||||||
public alterEgo?: string
|
public alterEgo?: string
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
}
|
}
|
||||||
// #enddocregion
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// #docregion
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { SubmittedComponent } from './submitted.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ CommonModule],
|
||||||
|
declarations: [ SubmittedComponent ],
|
||||||
|
exports: [ CommonModule, SubmittedComponent ]
|
||||||
|
})
|
||||||
|
export class SharedModule { }
|
|
@ -0,0 +1,32 @@
|
||||||
|
// #docregion
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { Hero } from './hero';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'hero-submitted',
|
||||||
|
template: `
|
||||||
|
<div *ngIf="submitted">
|
||||||
|
<h2>You submitted the following:</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-3">Name</div>
|
||||||
|
<div class="col-xs-9 pull-left">{{ hero.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-3">Alter Ego</div>
|
||||||
|
<div class="col-xs-9 pull-left">{{ hero.alterEgo }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-3">Power</div>
|
||||||
|
<div class="col-xs-9 pull-left">{{ hero.power }}</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<button class="btn btn-default" (click)="onClick()">Edit</button>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
export class SubmittedComponent {
|
||||||
|
@Input() hero: Hero;
|
||||||
|
@Input() submitted = false;
|
||||||
|
@Output() submittedChange = new EventEmitter<boolean>();
|
||||||
|
onClick() { this.submittedChange.emit(false); }
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- #docplaster -->
|
|
||||||
<!-- #docregion -->
|
<!-- #docregion -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div [hidden]="submitted">
|
<div [hidden]="submitted">
|
||||||
|
@ -47,27 +46,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>
|
<button type="submit" class="btn btn-default"
|
||||||
<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>
|
[disabled]="!heroForm.form.valid">Submit</button>
|
||||||
|
<button type="button" class="btn btn-default"
|
||||||
|
(click)="newHero()">New Hero</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div [hidden]="!submitted">
|
<hero-submitted [hero]="model" [(submitted)]="submitted"></hero-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>
|
</div>
|
||||||
<!-- #enddocregion -->
|
|
|
@ -1,38 +1,45 @@
|
||||||
|
/* tslint:disable: member-ordering */
|
||||||
// #docplaster
|
// #docplaster
|
||||||
// #docregion
|
// #docregion
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
import { Hero } from './hero';
|
|
||||||
|
import { Hero } from '../shared/hero';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
moduleId: module.id,
|
||||||
selector: 'hero-form-template',
|
selector: 'hero-form-template',
|
||||||
templateUrl: 'app/hero-form-template.component.html'
|
templateUrl: 'hero-form-template.component.html'
|
||||||
})
|
})
|
||||||
// #docregion class
|
// #docregion class
|
||||||
export class HeroFormTemplateComponent {
|
export class HeroFormTemplateComponent {
|
||||||
|
|
||||||
powers = ['Really Smart', 'Super Flexible',
|
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
||||||
'Super Hot', 'Weather Changer'];
|
|
||||||
|
model = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
|
||||||
|
|
||||||
model = new Hero(18, 'Dr IQ', this.powers[0],
|
|
||||||
'Chuck Overstreet');
|
|
||||||
// #enddocregion class
|
|
||||||
submitted = false;
|
submitted = false;
|
||||||
|
|
||||||
onSubmit() { this.submitted = true; }
|
onSubmit() {
|
||||||
|
this.submitted = true;
|
||||||
|
}
|
||||||
|
// #enddocregion class
|
||||||
|
|
||||||
// Reset the form with a new hero AND restore 'pristine' class state
|
// Reset the form with a new hero AND restore 'pristine' class state
|
||||||
// by toggling 'active' flag which causes the form
|
// by toggling 'active' flag which causes the form
|
||||||
// to be removed/re-added in a tick via NgIf
|
// to be removed/re-added in a tick via NgIf
|
||||||
// TODO: Workaround until NgForm has a reset method (#6822)
|
// TODO: Workaround until NgForm has a reset method (#6822)
|
||||||
active = true;
|
active = true;
|
||||||
|
// #docregion class
|
||||||
|
|
||||||
newHero() {
|
newHero() {
|
||||||
this.model = new Hero(42, '', '');
|
this.model = new Hero(42, '', '');
|
||||||
|
// #enddocregion class
|
||||||
|
|
||||||
this.active = false;
|
this.active = false;
|
||||||
setTimeout(()=> this.active=true, 0);
|
setTimeout(() => this.active = true, 0);
|
||||||
}
|
|
||||||
// #docregion class
|
// #docregion class
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// #enddocregion class
|
// #enddocregion class
|
||||||
// #enddocregion
|
// #enddocregion
|
|
@ -0,0 +1,13 @@
|
||||||
|
// #docregion
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { HeroFormTemplateComponent } from './hero-form-template.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ SharedModule, FormsModule ],
|
||||||
|
declarations: [ HeroFormTemplateComponent ],
|
||||||
|
exports: [ HeroFormTemplateComponent ]
|
||||||
|
})
|
||||||
|
export class HeroFormTemplateModule { }
|
|
@ -1,25 +0,0 @@
|
||||||
// #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
|
|
|
@ -1,61 +0,0 @@
|
||||||
<!-- #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 -->
|
|
|
@ -1,107 +0,0 @@
|
||||||
// #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
|
|
|
@ -41,6 +41,12 @@
|
||||||
"hide": true
|
"hide": true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"form-validation": {
|
||||||
|
"title": "Form Validation",
|
||||||
|
"intro": "Validate user's form entries",
|
||||||
|
"hide": true
|
||||||
|
},
|
||||||
|
|
||||||
"rc4-to-rc5": {
|
"rc4-to-rc5": {
|
||||||
"title": "RC4 to RC5 Migration",
|
"title": "RC4 to RC5 Migration",
|
||||||
"intro": "Migrate your RC4 app to RC5 in minutes.",
|
"intro": "Migrate your RC4 app to RC5 in minutes.",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
!= partial("../../../_includes/_ts-temp")
|
|
@ -37,6 +37,11 @@
|
||||||
"intro": "Render dynamic forms with NgFormModel"
|
"intro": "Render dynamic forms with NgFormModel"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"form-validation": {
|
||||||
|
"title": "Form Validation",
|
||||||
|
"intro": "Validate user's form entries"
|
||||||
|
},
|
||||||
|
|
||||||
"rc4-to-rc5": {
|
"rc4-to-rc5": {
|
||||||
"title": "RC4 to RC5 Migration",
|
"title": "RC4 to RC5 Migration",
|
||||||
"intro": "Migrate your RC4 app to RC5 in minutes.",
|
"intro": "Migrate your RC4 app to RC5 in minutes.",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
!= partial("../../../_includes/_ts-temp")
|
|
@ -43,16 +43,16 @@
|
||||||
"intro": "Render dynamic forms with FormGroup"
|
"intro": "Render dynamic forms with FormGroup"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"form-validation": {
|
||||||
|
"title": "Form Validation",
|
||||||
|
"intro": "Validate user's form entries"
|
||||||
|
},
|
||||||
|
|
||||||
"rc4-to-rc5": {
|
"rc4-to-rc5": {
|
||||||
"title": "RC4 to RC5 Migration",
|
"title": "RC4 to RC5 Migration",
|
||||||
"intro": "Migrate your RC4 app to RC5 in minutes."
|
"intro": "Migrate your RC4 app to RC5 in minutes."
|
||||||
},
|
},
|
||||||
|
|
||||||
"validation": {
|
|
||||||
"title": "Validation",
|
|
||||||
"intro": "Validate user's form entries"
|
|
||||||
},
|
|
||||||
|
|
||||||
"set-document-title": {
|
"set-document-title": {
|
||||||
"title": "Set the Document Title",
|
"title": "Set the Document Title",
|
||||||
"intro": "Setting the document or window title using the Title service."
|
"intro": "Setting the document or window title using the Title service."
|
||||||
|
|
|
@ -2,91 +2,97 @@ include ../_util-fns
|
||||||
|
|
||||||
<a id="top"></a>
|
<a id="top"></a>
|
||||||
:marked
|
:marked
|
||||||
We want our data to be accurate and complete. By helping the user enter
|
We can improve overall data quality by validating user input for accuracy and completeness.
|
||||||
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
|
In this cookbook we show how to validate user input in the UI and display useful validation messages
|
||||||
template-driven forms and then the reactive forms approach.
|
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.
|
An Angular component consists 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
|
The first example demonstrates input validation entirely within the template.
|
||||||
moves the validation logic out of the template and into the component class, giving you more control and better unit testing.
|
The second example moves the validation logic out of the template and into the component class,
|
||||||
|
giving the developer more control and easier unit testing.
|
||||||
|
|
||||||
In these examples we use the form created in the [Forms chapter.](../guide/forms.html)
|
Both examples are based on the the sample form in the [Forms chapter.](../guide/forms.html)
|
||||||
|
|
||||||
<a id="toc"></a>
|
<a id="toc"></a>
|
||||||
:marked
|
:marked
|
||||||
## Table of contents
|
## Contents
|
||||||
|
|
||||||
[Template-Driven Forms Approach](#template-driven)
|
[Template-Driven Forms Approach](#template-driven)
|
||||||
|
|
||||||
[Reactive Forms Approach](#model-driven)
|
[Reactive Forms Approach](#reactive)
|
||||||
|
|
||||||
:marked
|
**Try the live example**
|
||||||
**See the [live example](/resources/live-examples/cb-validation/ts/plnkr.html)**.
|
<live-example name="cb-form-validation" embedded img="cookbooks/form-validation/plunker.png"></live-example>
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
<a id="template-driven"></a>
|
<a id="template-driven"></a>
|
||||||
:marked
|
:marked
|
||||||
## Template-Driven Forms Approach
|
## Template-Driven Forms
|
||||||
|
|
||||||
Using the template-driven approach to form validation, the validation is defined in the template.
|
In the template-driven approach,
|
||||||
Each control on the form defines its validation, binding, and validation messages in the template.
|
each control on the form defines its own validation 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')
|
Here's an excerpt from the template html for a single input box control bound to the hero name:
|
||||||
|
+makeExample('cb-form-validation/ts/app/template/hero-form-template.component.html','name-with-error-msg','app/template/hero-form-template.component.html (Hero name)')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Here we define a standard label and set up an input box for validation of the hero name as follows:
|
Note the following:
|
||||||
- Add the desired HTML validation attributes. In this example,
|
- The `<input>` element implements the validation rules as HTML validation attributes: `required`, `minlength`, and `maxlength`.
|
||||||
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.
|
- Set the `name` attribute of the input box so Angular can track this input element.
|
||||||
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.
|
- The `[(ngModel)]` two-way data binding to the hero's name in the `model.name` property also
|
||||||
|
registers the input box as a control associated with the implicit `NgForm` directive.
|
||||||
|
|
||||||
|
- A template variable (`#name`) is a reference to this control.
|
||||||
|
that we can check for control states such as `valid` or `dirty`.
|
||||||
|
The template variable value is always `ngModel`.
|
||||||
|
|
||||||
|
- A `<div>` element for a group of validation error messages.
|
||||||
|
The `*ngIf` reveals the error group if there are any errors and
|
||||||
|
the control is either `dirty` or `touched`.
|
||||||
|
|
||||||
|
- Within the error group are separate `<div>` elements for each possible validation error.
|
||||||
|
Here we've prepared messages for `required`, `minlength`, and `maxlength`.
|
||||||
|
|
||||||
|
The full template repeats this kind of layout for each data entry control on the form.
|
||||||
.l-sub-section
|
.l-sub-section
|
||||||
:marked
|
:marked
|
||||||
Adding the check for `dirty` or `touched` prevents display of errors before the user has a chance to edit the
|
We shouldn't show errors for a new hero before the user has had a chance to edit the value.
|
||||||
value. This is most useful when adding new data, such as a new hero.
|
The checks for `dirty` and `touched` prevent premature display of errors.
|
||||||
|
|
||||||
Learn about `dirty` and `touched` in the [Forms](../guide/forms.html) chapter.
|
Learn about `dirty` and `touched` in the [Forms](../guide/forms.html) chapter.
|
||||||
:marked
|
:marked
|
||||||
The component class manages the model used in the data binding. And provides any other code required by the view.
|
The component class manages the hero model used in the data binding
|
||||||
|
as well as other code to support the view.
|
||||||
|
|
||||||
+makeExample('cb-validation/ts/app/hero-form-template.component.ts','class','app/hero-form-template.component.ts')
|
+makeExample('cb-form-validation/ts/app/template/hero-form-template.component.ts','class','app/template/hero-form-template.component.ts')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
Use this template-driven validation technique when working with simple forms with simple validation scenarios.
|
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:
|
Here are the pertinent files for the template-driven approach:
|
||||||
|
|
||||||
+makeTabs(
|
+makeTabs(
|
||||||
`cb-validation/ts/app/main.ts,
|
`cb-form-validation/ts/app/template/hero-form-template.module.ts,
|
||||||
cb-validation/ts/app/app.module.ts,
|
cb-form-validation/ts/app/template/hero-form-template.component.html,
|
||||||
cb-validation/ts/app/app.component.ts,
|
cb-form-validation/ts/app/template/hero-form-template.component.ts,
|
||||||
cb-validation/ts/app/hero.ts,
|
cb-form-validation/ts/app/shared/hero.ts,
|
||||||
cb-validation/ts/app/hero-form-template.component.html,
|
cb-form-validation/ts/app/shared/submitted.component.ts`,
|
||||||
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' )
|
`app/template/hero-form-template.module.ts,
|
||||||
|
app/template/hero-form-template.component.html,
|
||||||
|
app/template/hero-form-template.component.ts,
|
||||||
|
app/shared/hero.ts,
|
||||||
|
app/shared/submitted.component.ts`)
|
||||||
|
|
||||||
.l-main-section
|
.l-main-section
|
||||||
<a id="model-driven"></a>
|
<a id="reactive"></a>
|
||||||
:marked
|
:marked
|
||||||
## Reactive Forms Approach
|
## Reactive Forms
|
||||||
|
|
||||||
The alternate way to implement forms in Angular is to use reactive forms, previously called `model-driven forms`.
|
Reactive forms are an alternate approach to form validation in the validation rules are specified in the model as
|
||||||
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.
|
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.
|
You can adjust the validation based on the application state or user.
|
||||||
Your code then becomes the source of truth for your validation.
|
Your code then becomes the source of truth for your validation.
|
||||||
|
@ -100,7 +106,7 @@ include ../_util-fns
|
||||||
When moving the validation attributes out of the HTML, we are no longer aria ready. Work is being done to
|
When moving the validation attributes out of the HTML, we are no longer aria ready. Work is being done to
|
||||||
address this.
|
address this.
|
||||||
|
|
||||||
+makeExample('cb-validation/ts/app/hero-form-model.component.ts','class','app/hero-form-model.component.ts')
|
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts','class','app/reactive/hero-form-reactive.component.ts')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
In the component's class, we define the form and our own data structures to manage definition and display of the validation messages:
|
In the component's class, we define the form and our own data structures to manage definition and display of the validation messages:
|
||||||
|
@ -153,7 +159,7 @@ include ../_util-fns
|
||||||
We'll use the form and `formError` collection properties in the template. Notice that when using the reactive forms approach,
|
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.
|
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')
|
+makeExample('cb-form-validation/ts/app/reactive/hero-form-reactive.component.html','name-with-error-msg','app/reactive/hero-form-reactive.component.html')
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
In the template, define a standard label and set up an input box for validation as follows:
|
In the template, define a standard label and set up an input box for validation as follows:
|
||||||
|
@ -173,17 +179,20 @@ include ../_util-fns
|
||||||
|
|
||||||
Use this technique when you want better control over the validation rules and messsages.
|
Use this technique when you want better control over the validation rules and messsages.
|
||||||
|
|
||||||
Here's the complete solution for the reactive forms approach:
|
Here are the pertinent files for the reactive forms approach:
|
||||||
|
|
||||||
+makeTabs(
|
+makeTabs(
|
||||||
`cb-validation/ts/app/main.ts,
|
`cb-form-validation/ts/app/reactive/hero-form-reactive.module.ts,
|
||||||
cb-validation/ts/app/app.module.ts,
|
cb-form-validation/ts/app/reactive/hero-form-reactive.component.html,
|
||||||
cb-validation/ts/app/app.component.ts,
|
cb-form-validation/ts/app/reactive/hero-form-reactive.component.ts,
|
||||||
cb-validation/ts/app/hero.ts,
|
cb-form-validation/ts/app/shared/hero.ts,
|
||||||
cb-validation/ts/app/hero-form-model.component.html,
|
cb-form-validation/ts/app/shared/submitted.component.ts`,
|
||||||
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' )
|
`app/reactive/hero-form-reactive.module.ts,
|
||||||
|
app/reactive/hero-form-reactive.component.html,
|
||||||
|
app/reactive/hero-form-reactive.component.ts,
|
||||||
|
app/shared/hero.ts,
|
||||||
|
app/shared/submitted.component.ts`)
|
||||||
|
|
||||||
:marked
|
:marked
|
||||||
[Back to top](#top)
|
[Back to top](#top)
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Loading…
Reference in New Issue