docs(aio): add async validation chapter (#25189)
Closes #22881 PR Close #25189
This commit is contained in:
@ -8,6 +8,7 @@ import { HeroFormTemplateComponent } from './template/hero-form-template.compone
|
||||
import { HeroFormReactiveComponent } from './reactive/hero-form-reactive.component';
|
||||
import { ForbiddenValidatorDirective } from './shared/forbidden-name.directive';
|
||||
import { IdentityRevealedValidatorDirective } from './shared/identity-revealed.directive';
|
||||
import { UniqueAlterEgoValidatorDirective } from './shared/alter-ego.directive';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -20,7 +21,8 @@ import { IdentityRevealedValidatorDirective } from './shared/identity-revealed.d
|
||||
HeroFormTemplateComponent,
|
||||
HeroFormReactiveComponent,
|
||||
ForbiddenValidatorDirective,
|
||||
IdentityRevealedValidatorDirective
|
||||
IdentityRevealedValidatorDirective,
|
||||
UniqueAlterEgoValidatorDirective
|
||||
],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
|
@ -0,0 +1,48 @@
|
||||
/* tslint:disable: member-ordering forin */
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
|
||||
import { UniqueAlterEgoValidator } from '../shared/alter-ego.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-form-reactive',
|
||||
templateUrl: './hero-form-reactive.component.html',
|
||||
styleUrls: ['./hero-form-reactive.component.css'],
|
||||
})
|
||||
export class HeroFormReactiveComponent implements OnInit {
|
||||
|
||||
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
||||
|
||||
hero = { name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0] };
|
||||
|
||||
heroForm: FormGroup;
|
||||
|
||||
ngOnInit(): void {
|
||||
// #docregion async-validation
|
||||
this.heroForm = new FormGroup({
|
||||
'name': new FormControl(this.hero.name, [
|
||||
Validators.required,
|
||||
Validators.minLength(4),
|
||||
forbiddenNameValidator(/bob/i)
|
||||
]),
|
||||
'alterEgo': new FormControl(this.hero.alterEgo, {
|
||||
asyncValidators: [this.alterEgoValidator.validate.bind(this.alterEgoValidator)],
|
||||
updateOn: 'blur'
|
||||
}),
|
||||
'power': new FormControl(this.hero.power, Validators.required)
|
||||
});
|
||||
// #enddocregion async-validation
|
||||
}
|
||||
|
||||
get name() { return this.heroForm.get('name'); }
|
||||
|
||||
get power() { return this.heroForm.get('power'); }
|
||||
|
||||
get alterEgo() { return this.heroForm.get('alterEgo'); }
|
||||
|
||||
// #docregion async-validation
|
||||
constructor(private alterEgoValidator: UniqueAlterEgoValidator) {}
|
||||
// #enddocregion async-validation
|
||||
}
|
@ -35,6 +35,13 @@
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input id="alterEgo" class="form-control"
|
||||
formControlName="alterEgo" >
|
||||
|
||||
<div *ngIf="alterEgo.pending">Validating...</div>
|
||||
<div *ngIf="alterEgo.invalid" class="alert alert-danger alter-ego-errors">
|
||||
<div *ngIf="alterEgo.errors?.uniqueAlterEgo">
|
||||
Alter ego is already taken.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- #docregion cross-validation-error-message -->
|
||||
|
@ -4,6 +4,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
|
||||
import { identityRevealedValidator } from '../shared/identity-revealed.directive';
|
||||
import { UniqueAlterEgoValidator } from '../shared/alter-ego.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-form-reactive',
|
||||
@ -25,7 +26,10 @@ export class HeroFormReactiveComponent implements OnInit {
|
||||
Validators.minLength(4),
|
||||
forbiddenNameValidator(/bob/i)
|
||||
]),
|
||||
'alterEgo': new FormControl(this.hero.alterEgo),
|
||||
'alterEgo': new FormControl(this.hero.alterEgo, {
|
||||
asyncValidators: [this.alterEgoValidator.validate.bind(this.alterEgoValidator)],
|
||||
updateOn: 'blur'
|
||||
}),
|
||||
'power': new FormControl(this.hero.power, Validators.required)
|
||||
}, { validators: identityRevealedValidator }); // <-- add custom validator at the FormGroup level
|
||||
}
|
||||
@ -33,4 +37,8 @@ export class HeroFormReactiveComponent implements OnInit {
|
||||
get name() { return this.heroForm.get('name'); }
|
||||
|
||||
get power() { return this.heroForm.get('power'); }
|
||||
|
||||
get alterEgo() { return this.heroForm.get('alterEgo'); }
|
||||
|
||||
constructor(private alterEgoValidator: UniqueAlterEgoValidator) { }
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
import { Directive, forwardRef, Injectable } from '@angular/core';
|
||||
import {
|
||||
AsyncValidator,
|
||||
AbstractControl,
|
||||
NG_ASYNC_VALIDATORS,
|
||||
ValidationErrors
|
||||
} from '@angular/forms';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { HeroesService } from './heroes.service';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// #docregion async-validator
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UniqueAlterEgoValidator implements AsyncValidator {
|
||||
constructor(private heroesService: HeroesService) {}
|
||||
|
||||
validate(
|
||||
ctrl: AbstractControl
|
||||
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
|
||||
return this.heroesService.isAlterEgoTaken(ctrl.value).pipe(
|
||||
map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
|
||||
catchError(() => null)
|
||||
);
|
||||
}
|
||||
}
|
||||
// #enddocregion async-validator
|
||||
|
||||
// #docregion async-validator-directive
|
||||
@Directive({
|
||||
selector: '[appUniqueAlterEgo]',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_ASYNC_VALIDATORS,
|
||||
useExisting: forwardRef(() => UniqueAlterEgoValidator),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class UniqueAlterEgoValidatorDirective {
|
||||
constructor(private validator: UniqueAlterEgoValidator) {}
|
||||
|
||||
validate(control: AbstractControl) {
|
||||
this.validator.validate(control);
|
||||
}
|
||||
}
|
||||
// #enddocregion async-validator-directive
|
@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { delay } from 'rxjs/operators';
|
||||
|
||||
const ALTER_EGOS = ['Eric'];
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HeroesService {
|
||||
isAlterEgoTaken(alterEgo: string): Observable<boolean> {
|
||||
const isTaken = ALTER_EGOS.includes(alterEgo);
|
||||
|
||||
return of(isTaken).pipe(delay(400));
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion name-with-error-msg -->
|
||||
<!-- #docregion name-input -->
|
||||
<input id="name" name="name" class="form-control"
|
||||
<input id="name" name="name" class="form-control"
|
||||
required minlength="4" appForbiddenName="bob"
|
||||
[(ngModel)]="hero.name" #name="ngModel" >
|
||||
<!-- #enddocregion name-input -->
|
||||
@ -35,8 +35,20 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<input id="alterEgo" class="form-control"
|
||||
name="alterEgo" [(ngModel)]="hero.alterEgo" >
|
||||
<!-- #docregion async-validation -->
|
||||
<input id="alterEgo" class="form-control" name="alterEgo"
|
||||
#alterEgo="ngModel"
|
||||
[(ngModel)]="hero.alterEgo"
|
||||
[ngModelOptions]="{ updateOn: 'blur' }"
|
||||
appUniqueAlterEgo>
|
||||
<!-- #enddocregion async-validation -->
|
||||
|
||||
<div *ngIf="alterEgo.pending">Validating...</div>
|
||||
<div *ngIf="alterEgo.invalid" class="alert alert-danger alter-ego-errors">
|
||||
<div *ngIf="alterEgo.errors?.uniqueAlterEgo">
|
||||
Alter ego is already taken.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- #docregion cross-validation-error-message -->
|
||||
|
Reference in New Issue
Block a user