parent
002a5afa98
commit
5feb9e1935
|
@ -3,27 +3,17 @@
|
|||
|
||||
<h1>Reactive Form</h1>
|
||||
|
||||
<!-- #docregion cross-validation -->
|
||||
<form [formGroup]="heroForm" #formDir="ngForm">
|
||||
<!-- #enddocregion cross-validation -->
|
||||
|
||||
<div [hidden]="formDir.submitted">
|
||||
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-class -->
|
||||
<div class="cross-validation" [class.cross-validation-error]="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)">
|
||||
<!-- #enddocregion cross-validation-error-class -->
|
||||
<!-- #enddocregion cross-validation -->
|
||||
<div class="form-group">
|
||||
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion name-with-error-msg -->
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-class-->
|
||||
<input id="name" class="form-control"
|
||||
formControlName="name" required >
|
||||
<!-- #enddocregion cross-validation -->
|
||||
<!-- #enddocregion cross-validation-error-class-->
|
||||
|
||||
<div *ngIf="name.invalid && (name.dirty || name.touched)"
|
||||
class="alert alert-danger">
|
||||
|
@ -43,27 +33,16 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-class-->
|
||||
<input id="alterEgo" class="form-control"
|
||||
formControlName="alterEgo" >
|
||||
<!-- #enddocregion cross-validation -->
|
||||
<!-- #enddocregion cross-validation-error-class -->
|
||||
</div>
|
||||
|
||||
<!-- #docregion cross-validation-error-message -->
|
||||
<!-- #docregion cross-validation -->
|
||||
<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
|
||||
Name cannot match alter ego.
|
||||
</div>
|
||||
<!-- #enddocregion cross-validation -->
|
||||
<!-- #enddocregion cross-validation-error-message -->
|
||||
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-class-->
|
||||
</div>
|
||||
<!-- #enddocregion cross-validation -->
|
||||
<!-- #enddocregion cross-validation-error-class -->
|
||||
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
|
@ -82,9 +61,7 @@
|
|||
<button type="button" class="btn btn-default"
|
||||
(click)="formDir.resetForm({})">Reset</button>
|
||||
</div>
|
||||
<!-- #docregion cross-validation -->
|
||||
</form>
|
||||
<!-- #enddocregion cross-validation -->
|
||||
|
||||
<div class="submitted-message" *ngIf="formDir.submitted">
|
||||
<p>You've submitted your hero, {{ heroForm.value.name }}!</p>
|
||||
|
|
|
@ -5,23 +5,19 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
|
|||
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
|
||||
import { identityRevealedValidator } from '../shared/identity-revealed.directive';
|
||||
|
||||
// #docregion cross-validation-component
|
||||
@Component({
|
||||
selector: 'app-hero-form-reactive',
|
||||
templateUrl: './hero-form-reactive.component.html',
|
||||
styleUrls: ['./hero-form-reactive.component.css'],
|
||||
})
|
||||
export class HeroFormReactiveComponent implements OnInit {
|
||||
// #enddocregion cross-validation-component
|
||||
|
||||
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
|
||||
|
||||
hero = { name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0] };
|
||||
|
||||
// #docregion cross-validation-component
|
||||
heroForm: FormGroup;
|
||||
|
||||
// #docregion cross-validation-register
|
||||
ngOnInit(): void {
|
||||
this.heroForm = new FormGroup({
|
||||
'name': new FormControl(this.hero.name, [
|
||||
|
@ -33,12 +29,8 @@ export class HeroFormReactiveComponent implements OnInit {
|
|||
'power': new FormControl(this.hero.power, Validators.required)
|
||||
}, { validators: identityRevealedValidator }); // <-- add custom validator at the FormGroup level
|
||||
}
|
||||
// #enddocregion cross-validation-register
|
||||
// #enddocregion cross-validation-component
|
||||
|
||||
get name() { return this.heroForm.get('name'); }
|
||||
|
||||
get power() { return this.heroForm.get('power'); }
|
||||
// #docregion cross-validation-component
|
||||
}
|
||||
// #enddocregion cross-validation-component
|
||||
|
|
|
@ -2,14 +2,13 @@
|
|||
import { Directive } from '@angular/core';
|
||||
import { AbstractControl, FormGroup, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn } from '@angular/forms';
|
||||
|
||||
// #docregion cross-validation-directive-with-validator
|
||||
// #docregion cross-validation-validator
|
||||
/** A hero's name can't match the hero's alter ego */
|
||||
export const identityRevealedValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => {
|
||||
const name = control.get('name');
|
||||
const alterEgo = control.get('alterEgo');
|
||||
|
||||
return name && alterEgo && name.value !== alterEgo.value ? null : { 'identityRevealed': { value: true } };
|
||||
return name && alterEgo && name.value !== alterEgo.value ? null : { 'identityRevealed': true };
|
||||
};
|
||||
// #enddocregion cross-validation-validator
|
||||
|
||||
|
@ -24,5 +23,3 @@ export class IdentityRevealedValidatorDirective implements Validator {
|
|||
}
|
||||
}
|
||||
// #enddocregion cross-validation-directive
|
||||
// #enddocregion cross-validation-directive-with-validator
|
||||
|
||||
|
|
|
@ -2,28 +2,18 @@
|
|||
<div class="container">
|
||||
|
||||
<h1>Template-Driven Form</h1>
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-register-validator -->
|
||||
<form #heroForm="ngForm" appIdentityRevealed>
|
||||
<!-- #enddocregion cross-validation-register-validator -->
|
||||
<!-- #enddocregion cross-validation -->
|
||||
<div [hidden]="heroForm.submitted">
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-class -->
|
||||
<div class="cross-validation" [class.cross-validation-error]="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)">
|
||||
<!-- #enddocregion cross-validation-error-class -->
|
||||
<!-- #enddocregion cross-validation -->
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<!-- #docregion name-with-error-msg -->
|
||||
<!-- #docregion name-input -->
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-class -->
|
||||
<input id="name" name="name" class="form-control"
|
||||
required minlength="4" appForbiddenName="bob"
|
||||
[(ngModel)]="hero.name" #name="ngModel" >
|
||||
<!-- #enddocregion cross-validation-error-class-->
|
||||
<!-- #enddocregion cross-validation-->
|
||||
<!-- #enddocregion name-input -->
|
||||
|
||||
<div *ngIf="name.invalid && (name.dirty || name.touched)"
|
||||
|
@ -45,28 +35,16 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label for="alterEgo">Alter Ego</label>
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-class -->
|
||||
<input id="alterEgo" class="form-control"
|
||||
name="alterEgo" [(ngModel)]="hero.alterEgo" >
|
||||
<!-- #enddocregion cross-validation-error-class -->
|
||||
<!-- #enddocregion cross-validation -->
|
||||
</div>
|
||||
|
||||
<!-- #docregion cross-validation -->
|
||||
<!-- #docregion cross-validation-error-message -->
|
||||
<div *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)" class="cross-validation-error-message alert alert-danger">
|
||||
Name cannot match alter ego.
|
||||
</div>
|
||||
<!-- #enddocregion cross-validation-error-message -->
|
||||
<!-- #enddocregion cross-validation -->
|
||||
|
||||
<!-- #docregion cross-validation-->
|
||||
<!-- #docregion cross-validation-error-class -->
|
||||
</div>
|
||||
<!-- #enddocregion cross-validation-error-class -->
|
||||
<!-- #enddocregion cross-validation -->
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="power">Hero Power</label>
|
||||
|
@ -90,9 +68,5 @@
|
|||
<p>You've submitted your hero, {{ heroForm.value.name }}!</p>
|
||||
<button (click)="heroForm.resetForm({})">Add new hero</button>
|
||||
</div>
|
||||
|
||||
<!-- #docregion cross-validation -->
|
||||
</form>
|
||||
<!-- #enddocregion cross-validation -->
|
||||
|
||||
</div>
|
||||
|
|
|
@ -213,27 +213,33 @@ This section shows how to perform cross field validation. It assumes some basic
|
|||
|
||||
<div class="l-sub-section">
|
||||
|
||||
If you haven't created custom validators before, start by reviewing the [CustomValidators](guide/form-validation#custom-validators).
|
||||
If you haven't created custom validators before, start by reviewing the [custom validators section](guide/form-validation#custom-validators).
|
||||
|
||||
</div>
|
||||
|
||||
In the following section we will make sure that our heroes do not reveal their true identities by filling out the Hero Form. We will do that by validating that the hero names and alter egos do not match. The form has the following structure:
|
||||
In the following section, we will make sure that our heroes do not reveal their true identities by filling out the Hero Form. We will do that by validating that the hero names and alter egos do not match.
|
||||
|
||||
### Adding to reactive forms
|
||||
|
||||
The form has the following structure:
|
||||
|
||||
```javascript
|
||||
const heroForm = new FormGroup({
|
||||
'name': new FormControl(),
|
||||
'alterEgo': new FormControl(),
|
||||
'power': new FormControl()
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
Notice that the `name` and `alterEgo` are sibling controls. To evaluate both controls in a single custom validator, we should perform the validation in the ancestor control. That way we can query the form tree for the child controls which will allow us to compare their values.
|
||||
Notice that the name and alterEgo are sibling controls. To evaluate both controls in a single custom validator, we should perform the validation in a common ancestor control: the FormGroup. That way, we can query the FormGroup for the child controls which will allow us to compare their values.
|
||||
|
||||
To add a validator to the FormGroup, pass the new validator in as the second argument on creation.
|
||||
|
||||
```javascript
|
||||
const heroForm = new FormGroup({
|
||||
'name': new FormControl(),
|
||||
'alterEgo': new FormControl(),
|
||||
'power': new FormControl()
|
||||
'alterEgo': new FormControl(),
|
||||
'power': new FormControl()
|
||||
}, { validators: identityRevealedValidator });
|
||||
```
|
||||
|
||||
|
@ -242,18 +248,11 @@ The validator code is as follows:
|
|||
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-validator" title="shared/identity-revealed.directive.ts" linenums="false">
|
||||
</code-example>
|
||||
|
||||
Identity validator implements the `ValidatorFn` interface. It takes an Angular control object as an argument and returns either null if the form is valid, or ValidationErrors otherwise.
|
||||
Identity validator implements the `ValidatorFn` interface. It takes an Angular control object as an argument and returns either null if the form is valid, or `ValidationErrors` otherwise.
|
||||
|
||||
First we retrieve the child controls by calling the `FormGroup`'s [get](api/forms/AbstractControl#get) method. Then we simply compare the values of the `name` and `alterEgo` controls.
|
||||
|
||||
If the values do not match the hero's identity remains disguised, and we can safely return null. Otherwise, the hero's identity is revealed and we must mark the form as invalid by returning an error object.
|
||||
|
||||
|
||||
### Adding to reactive forms
|
||||
|
||||
As with all reactive forms, the validator can be registered during the form creation. We make sure to register it at the `FormGroup` level, to give it access to the child controls.
|
||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="cross-validation-register" title="reactive/hero-form-template.component.ts" linenums="false">
|
||||
</code-example>
|
||||
If the values do not match, the hero's identity remains secret, and we can safely return null. Otherwise, the hero's identity is revealed and we must mark the form as invalid by returning an error object.
|
||||
|
||||
Next, to provide better user experience, we show an appropriate error message when the form is invalid.
|
||||
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="cross-validation-error-message" title="reactive/hero-form-template.component.html" linenums="false">
|
||||
|
@ -263,34 +262,10 @@ Note that we check if:
|
|||
- the `FormGroup` has the cross validation error returned by the `identityRevealed` validator,
|
||||
- the user is yet to [interact](guide/form-validation#why-check-dirty-and-touched) with the form.
|
||||
|
||||
Finally, we want to style the `input` elements to give visual feedback about form control's validity. Unfortunately, the `identityRevealed` error is on the form group control (`heroForm`) and not on the child form controls (`name`, `alterEgo`). That means that the `<input formControlName="name" />` and `<input formControlName="alterEgo" />` will not have the `ng-invalid` class after the cross validation fails.
|
||||
|
||||
We can get around this problem by binding our own css class to the same expression that was used to show and hide the validation error message.
|
||||
|
||||
<code-tabs linenums="false">
|
||||
<code-pane path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="cross-validation-error-class" title="reactive/hero-form-reactive.component.html" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/reactive/hero-form-reactive.component.css" title="reactive/hero-form-reactive.component.css" linenums="false">
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
|
||||
Below you can inspect the cross validation example for the reactive forms. The example skips over the parts that are not related to cross validation to keep this section focused on a single task. You can see the complete code example at the end of this chapter.
|
||||
|
||||
<code-tabs linenums="false">
|
||||
<code-pane path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="cross-validation" title="reactive/hero-form-reactive.component.html" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/reactive/hero-form-reactive.component.ts" region="cross-validation-component" title="reactive/hero-form-reactive.component.ts" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/reactive/hero-form-reactive.component.css" title="reactive/hero-form-reactive.component.css" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/shared/identity-revealed.directive.ts" title="shared/identity-revealed.directive.ts" region="cross-validation-validator" linenums="false">
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
|
||||
### Adding to template driven forms
|
||||
First we must create a directive that will wrap the validator function. We provide it as the validator using the `NG_VALIDATORS` token. If you are not sure why, or you do not fully understand the syntax revisit the previous [section](guide/form-validation#adding-to-template-driven-forms).
|
||||
First we must create a directive that will wrap the validator function. We provide it as the validator using the `NG_VALIDATORS` token. If you are not sure why, or you do not fully understand the syntax, revisit the previous [section](guide/form-validation#adding-to-template-driven-forms).
|
||||
|
||||
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-directive" title="shared/identity-revealed.directive.ts (directive)" linenums="false">
|
||||
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-directive" title="shared/identity-revealed.directive.ts" linenums="false">
|
||||
</code-example>
|
||||
|
||||
Next, we have to add the directive to the html template. Since the validator must be registered at the `FormGroup` level, we put the directive on the `form` tag.
|
||||
|
@ -305,32 +280,8 @@ Note that we check if:
|
|||
- the `FormGroup` has the cross validation error returned by the `identityRevealed` validator,
|
||||
- the user is yet to [interact](guide/form-validation#why-check-dirty-and-touched) with the form.
|
||||
|
||||
Finally, we want to style the `input` elements to give visual feedback about form control's validity. Unfortunately, the `identityRevealed` error is on the form group control (`heroForm`) and not on the child form controls (`name`, `alterEgo`). That means that the `<input [(ngModel)]="hero.name" />` and `<input [(ngModel)]="hero.alterEgo" />` will not have the `ng-invalid` class after the cross validation fails.
|
||||
|
||||
We can get around this problem by binding our own css class to the same expression that was used to show and hide the validation error message.
|
||||
|
||||
<code-tabs linenums="false">
|
||||
<code-pane path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-error-class" title="template/hero-form-template.component.html" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/template/hero-form-template.component.css" title="template/hero-form-template.component.css" linenums="false">
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
|
||||
Below you can inspect the cross validation example for the template-driven forms. The example skips over parts that are not related to cross validation to keep this section focused on a single task. You can see the complete code example at the end of this chapter.
|
||||
<code-tabs linenums="false">
|
||||
<code-pane path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation" title="template/hero-form-template.component.html" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/template/hero-form-template.component.ts" region="component" title="template/hero-form-template.component.ts" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/template/hero-form-template.component.css" title="template/hero-form-template.component.css" linenums="false">
|
||||
</code-pane>
|
||||
<code-pane path="form-validation/src/app/shared/identity-revealed.directive.ts" title="shared/identity-revealed.directive.ts" region="cross-validation-directive-with-validator" linenums="false">
|
||||
</code-pane>
|
||||
</code-tabs>
|
||||
|
||||
This completes the cross validation example. We managed to:
|
||||
- validate the form based on the values of two sibling controls,
|
||||
- show a descriptive error message after the user interacted with the form and the validation failed,
|
||||
- give visual feedback on the form validity.
|
||||
- show a descriptive error message after the user interacted with the form and the validation failed.
|
||||
|
||||
**You can run the <live-example></live-example> to see the complete reactive and template-driven example code.**
|
||||
|
|
Loading…
Reference in New Issue