docs: refactor form validation doc to be task-oriented (#36374)

rework content to meet current documentation standards and conventions, structure as task-oriented document

PR Close #36374
This commit is contained in:
Judy Bogart 2020-03-31 13:28:42 -07:00 committed by Misko Hevery
parent 28f3c1c96a
commit 7eb1a58b26
2 changed files with 143 additions and 131 deletions

View File

@ -1,132 +1,131 @@
# Form validation # Validating form input
Improve overall data quality by validating user input for accuracy and completeness. You can improve overall data quality by validating user input for accuracy and completeness.
This page shows how to validate user input from the UI and display useful validation messages,
in both reactive and template-driven forms.
This page shows how to validate user input in the UI and display useful validation messages **Prerequisites**
using both reactive and template-driven forms. It assumes some basic knowledge of the two
forms modules. Before reading about form validation, you should have a basic understanding of the following.
* [TypeScript](https://www.typescriptlang.org/docs/home.html "The TypeScript language") and HTML5 programming.
* Fundamental concepts of [Angular app design](guide/architecture "Introduction to Angular app-design concepts").
* The [two types of forms that Angular supports](guide/forms-overview "Introduction to Angular forms").
* Basics of either [Template-driven Forms](guide/forms "Template-driven forms guide") or [Reactive Forms](guide/reactive-forms "Reactive forms guide").
<div class="alert is-helpful"> <div class="alert is-helpful">
For the sample app that this page describes, see the <live-example></live-example>. Get the complete example code for the reactive and template-driven forms used here to illustrate form validation.
Run the <live-example></live-example>.
</div> </div>
<div class="alert is-helpful"> {@a template-driven-validation}
If you're new to forms, start by reviewing the [Forms](guide/forms) and ## Validating input in template-driven forms
[Reactive Forms](guide/reactive-forms) guides.
</div>
## Template-driven validation
To add validation to a template-driven form, you add the same validation attributes as you To add validation to a template-driven form, you add the same validation attributes as you
would with [native HTML form validation](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation). would with [native HTML form validation](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/HTML5/Constraint_validation).
Angular uses directives to match these attributes with validator functions in the framework. Angular uses directives to match these attributes with validator functions in the framework.
Every time the value of a form control changes, Angular runs validation and generates Every time the value of a form control changes, Angular runs validation and generates
either a list of validation errors, which results in an INVALID status, or null, which results in a VALID status. either a list of validation errors that results in an INVALID status, or null, which results in a VALID status.
You can then inspect the control's state by exporting `ngModel` to a local template variable. You can then inspect the control's state by exporting `ngModel` to a local template variable.
The following example exports `NgModel` into a variable called `name`: The following example exports `NgModel` into a variable called `name`:
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-with-error-msg" header="template/hero-form-template.component.html (name)"></code-example> <code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-with-error-msg" header="template/hero-form-template.component.html (name)"></code-example>
Notice the following features illustrated by the example.
Note the following:
* The `<input>` element carries the HTML validation attributes: `required` and `minlength`. It * The `<input>` element carries the HTML validation attributes: `required` and `minlength`. It
also carries a custom validator directive, `forbiddenName`. For more also carries a custom validator directive, `forbiddenName`. For more
information, see [Custom validators](guide/form-validation#custom-validators) section. information, see the [Custom validators](#custom-validators) section.
* `#name="ngModel"` exports `NgModel` into a local variable called `name`. `NgModel` mirrors many of the properties of its underlying * `#name="ngModel"` exports `NgModel` into a local variable called `name`. `NgModel` mirrors many of the properties of its underlying
`FormControl` instance, so you can use this in the template to check for control states such as `valid` and `dirty`. For a full list of control properties, see the [AbstractControl](api/forms/AbstractControl) `FormControl` instance, so you can use this in the template to check for control states such as `valid` and `dirty`. For a full list of control properties, see the [AbstractControl](api/forms/AbstractControl)
API reference. API reference.
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs` * The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
but only if the `name` is invalid and the control is either `dirty` or `touched`. but only if the `name` is invalid and the control is either `dirty` or `touched`.
* Each nested `<div>` can present a custom message for one of the possible validation errors. * Each nested `<div>` can present a custom message for one of the possible validation errors.
There are messages for `required`, `minlength`, and `forbiddenName`. There are messages for `required`, `minlength`, and `forbiddenName`.
{@a dirty-or-touched}
<div class="alert is-helpful"> <div class="alert is-helpful">
To prevent the validator from displaying errors before the user has a chance to edit the form, you should check for either the `dirty` or `touched` states in a control.
* When the user changes the value in the watched field, the control is marked as "dirty".
#### Why check _dirty_ and _touched_? * When the user blurs the form control element, the control is marked as "touched".
You may not want your application to display errors before the user has a chance to edit the form.
The checks for `dirty` and `touched` prevent errors from showing until the user
does one of two things: changes the value,
turning the control dirty; or blurs the form control element, setting the control to touched.
</div> </div>
## Reactive form validation {@a reactive-form-validation}
In a reactive form, the source of truth is the component class. Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class. Angular then calls these functions whenever the value of the control changes. ## Validating input in reactive forms
In a reactive form, the source of truth is the component class.
Instead of adding validators through attributes in the template, you add validator functions directly to the form control model in the component class.
Angular then calls these functions whenever the value of the control changes.
### Validator functions ### Validator functions
There are two types of validator functions: sync validators and async validators. Validator functions can be either synchronous or asynchronous.
* **Sync validators**: functions that take a control instance and immediately return either a set of validation errors or `null`. You can pass these in as the second argument when you instantiate a `FormControl`. * **Sync validators**: Synchronous functions that take a control instance and immediately return either a set of validation errors or `null`. You can pass these in as the second argument when you instantiate a `FormControl`.
* **Async validators**: functions that take a control instance and return a Promise * **Async validators**: Asynchronous functions that take a control instance and return a Promise
or Observable that later emits a set of validation errors or `null`. You can or Observable that later emits a set of validation errors or `null`. You can
pass these in as the third argument when you instantiate a `FormControl`. pass these in as the third argument when you instantiate a `FormControl`.
Note: for performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set. For performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set.
### Built-in validators ### Built-in validator functions
You can choose to [write your own validator functions](guide/form-validation#custom-validators), or you can use some of You can choose to [write your own validator functions](#custom-validators), or you can use some of Angular's built-in validators.
Angular's built-in validators.
The same built-in validators that are available as attributes in template-driven forms, such as `required` and `minlength`, are all available to use as functions from the `Validators` class. For a full list of built-in validators, see the [Validators](api/forms/Validators) API reference. The same built-in validators that are available as attributes in template-driven forms, such as `required` and `minlength`, are all available to use as functions from the `Validators` class.
For a full list of built-in validators, see the [Validators](api/forms/Validators) API reference.
To update the hero form to be a reactive form, you can use some of the same To update the hero form to be a reactive form, you can use some of the same
built-in validators&mdash;this time, in function form. See below: built-in validators&mdash;this time, in function form, as in the following example.
{@a reactive-component-class} {@a reactive-component-class}
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="form-group" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example> <code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="form-group" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example>
Note that: In this example, the `name` control sets up two built-in validators&mdash;`Validators.required` and `Validators.minLength(4)`&mdash;and one custom validator, `forbiddenNameValidator`. (For more details see [custom validators](#custom-validators) below.)
* The name control sets up two built-in validators&mdash;`Validators.required` and `Validators.minLength(4)`&mdash;and one custom validator, `forbiddenNameValidator`. For more details see the [Custom validators](guide/form-validation#custom-validators) section in this guide. All of these validators are synchronous, so they are passed as the second argument. Notice that you can support multiple validators by passing the functions in as an array.
* As these validators are all sync validators, you pass them in as the second argument.
* Support multiple validators by passing the functions in as an array.
* This example adds a few getter methods. In a reactive form, you can always access any form control through the `get` method on its parent group, but sometimes it's useful to define getters as shorthands
for the template.
This example also adds a few getter methods. In a reactive form, you can always access any form control through the `get` method on its parent group, but sometimes it's useful to define getters as shorthand for the template.
If you look at the template for the name input again, it is fairly similar to the template-driven example. If you look at the template for the `name` input again, it is fairly similar to the template-driven example.
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="name-with-error-msg" header="reactive/hero-form-reactive.component.html (name with error msg)"></code-example> <code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="name-with-error-msg" header="reactive/hero-form-reactive.component.html (name with error msg)"></code-example>
Key takeaways: This form differs from the template-driven version in that it no longer exports any directives. Instead, it uses the `name` getter defined in the component class.
* The form no longer exports any directives, and instead uses the `name` getter defined in Notice that the `required` attribute is still present in the template. Although it's not necessary for validation, it should be retained to for accessibility purposes.
the component class.
* The `required` attribute is still present. While it's not necessary for validation purposes,
you may want to keep it in your template for CSS styling or accessibility reasons.
{@a custom-validators}
## Custom validators ## Defining custom validators
Since the built-in validators won't always match the exact use case of your application, sometimes you'll want to create a custom validator. The built-in validators don't always match the exact use case of your application, so you sometimes need to create a custom validator.
Consider the `forbiddenNameValidator` function from previous Consider the `forbiddenNameValidator` function from previous [reactive-form examples](#reactive-component-class).
[examples](guide/form-validation#reactive-component-class) in Here's what the definition of that function looks like.
this guide. Here's what the definition of that function looks like:
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="custom-validator" header="shared/forbidden-name.directive.ts (forbiddenNameValidator)"></code-example> <code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="custom-validator" header="shared/forbidden-name.directive.ts (forbiddenNameValidator)"></code-example>
The function is actually a factory that takes a regular expression to detect a _specific_ forbidden name and returns a validator function. The function is a factory that takes a regular expression to detect a _specific_ forbidden name and returns a validator function.
In this sample, the forbidden name is "bob", so the validator will reject any hero name containing "bob". In this sample, the forbidden name is "bob", so the validator will reject any hero name containing "bob".
Elsewhere it could reject "alice" or any name that the configuring regular expression matches. Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
@ -137,55 +136,57 @@ null if the control value is valid _or_ a validation error object.
The validation error object typically has a property whose name is the validation key, `'forbiddenName'`, The validation error object typically has a property whose name is the validation key, `'forbiddenName'`,
and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`. and whose value is an arbitrary dictionary of values that you could insert into an error message, `{name}`.
Custom async validators are similar to sync validators, but they must instead return a Promise or Observable Custom async validators are similar to sync validators, but they must instead return a Promise or observable that later emits null or a validation error object.
that later emits null or a validation error object. In the case of an Observable, the Observable must complete, In the case of an observable, the observable must complete, at which point the form uses the last value emitted for validation.
at which point the form uses the last value emitted for validation.
### Adding to reactive forms {@a adding-to-reactive-forms}
In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly ### Adding custom validators to reactive forms
to the `FormControl`.
In reactive forms, add a custom validator by passing the function directly to the `FormControl`.
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="custom-validator" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example> <code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="custom-validator" header="reactive/hero-form-reactive.component.ts (validator functions)"></code-example>
### Adding to template-driven forms {@a adding-to-template-driven-forms}
In template-driven forms, you don't have direct access to the `FormControl` instance, so you can't pass the ### Adding custom validators to template-driven forms
validator in like you can for reactive forms. Instead, you need to add a directive to the template.
The corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`. In template-driven forms, add a directive to the template, where the directive wraps the validator function.
For example, the corresponding `ForbiddenValidatorDirective` serves as a wrapper around the `forbiddenNameValidator`.
Angular recognizes the directive's role in the validation process because the directive registers itself Angular recognizes the directive's role in the validation process because the directive registers itself with the `NG_VALIDATORS` provider, as shown in the following example.
with the `NG_VALIDATORS` provider, a provider with an extensible collection of validators. `NG_VALIDATORS` is a predefined provider with an extensible collection of validators.
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" header="shared/forbidden-name.directive.ts (providers)"></code-example> <code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" header="shared/forbidden-name.directive.ts (providers)"></code-example>
The directive class then implements the `Validator` interface, so that it can easily integrate The directive class then implements the `Validator` interface, so that it can easily integrate
with Angular forms. Here is the rest of the directive to help you get an idea of how it all with Angular forms.
comes together: Here is the rest of the directive to help you get an idea of how it all
comes together.
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" header="shared/forbidden-name.directive.ts (directive)"> <code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" header="shared/forbidden-name.directive.ts (directive)">
</code-example> </code-example>
Once the `ForbiddenValidatorDirective` is ready, you can simply add its selector, `appForbiddenName`, to any input element to activate it. For example: Once the `ForbiddenValidatorDirective` is ready, you can add its selector, `appForbiddenName`, to any input element to activate it.
For example:
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-input" header="template/hero-form-template.component.html (forbidden-name-input)"></code-example> <code-example path="form-validation/src/app/template/hero-form-template.component.html" region="name-input" header="template/hero-form-template.component.html (forbidden-name-input)"></code-example>
<div class="alert is-helpful"> <div class="alert is-helpful">
You may have noticed that the custom validation directive is instantiated with `useExisting` Notice that the custom validation directive is instantiated with `useExisting` rather than `useClass`. The registered validator must be _this instance_ of
rather than `useClass`. The registered validator must be _this instance_ of
the `ForbiddenValidatorDirective`&mdash;the instance in the form with the `ForbiddenValidatorDirective`&mdash;the instance in the form with
its `forbiddenName` property bound to “bob". If you were to replace its `forbiddenName` property bound to “bob".
`useExisting` with `useClass`, then youd be registering a new class instance, one that
doesnt have a `forbiddenName`. If you were to replace `useExisting` with `useClass`, then youd be registering a new class instance, one that doesnt have a `forbiddenName`.
</div> </div>
## Control status CSS classes ## Control status CSS classes
Like in AngularJS, Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form. The following classes are currently supported: Angular automatically mirrors many control properties onto the form control element as CSS classes. You can use these classes to style form control elements according to the state of the form.
The following classes are currently supported.
* `.ng-valid` * `.ng-valid`
* `.ng-invalid` * `.ng-invalid`
@ -195,25 +196,27 @@ Like in AngularJS, Angular automatically mirrors many control properties onto th
* `.ng-untouched` * `.ng-untouched`
* `.ng-touched` * `.ng-touched`
The hero form uses the `.ng-valid` and `.ng-invalid` classes to In the following example, the hero form uses the `.ng-valid` and `.ng-invalid` classes to
set the color of each form control's border. set the color of each form control's border.
<code-example path="form-validation/src/assets/forms.css" header="forms.css (status classes)"> <code-example path="form-validation/src/assets/forms.css" header="forms.css (status classes)">
</code-example> </code-example>
## Cross field validation ## Cross-field validation
This section shows how to perform cross field validation. It assumes some basic knowledge of creating custom validators.
<div class="alert is-helpful"> A cross-field validator is a [custom validator](#custom-validators "Read about custom validators") that compares the values of different fields in a form and accepts or rejects them in combination.
For example, you might have a form that offers mutually incompatible options, so that if the user can choose A or B, but not both.
Some field values might also depend on others; a user might be allowed to choose B only if A is also chosen.
If you haven't created custom validators before, start by reviewing the [custom validators section](guide/form-validation#custom-validators). The following cross validation examples show how to do the following:
</div> * Validate reactive or template-based form input based on the values of two sibling controls,
* Show a descriptive error message after the user interacted with the form and the validation failed.
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 examples use cross-validation to ensure that heroes do not reveal their true identities by filling out the Hero Form. The validators do this by checking that the hero names and alter egos do not match.
### Adding to reactive forms ### Adding cross-validation to reactive forms
The form has the following structure: The form has the following structure:
@ -225,7 +228,9 @@ const heroForm = new FormGroup({
}); });
``` ```
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. Notice that the `name` and `alterEgo` are sibling controls.
To evaluate both controls in a single custom validator, you must perform the validation in a common ancestor control: the `FormGroup`.
You query the `FormGroup` for its child controls so that you can compare their values.
To add a validator to the `FormGroup`, pass the new validator in as the second argument on creation. To add a validator to the `FormGroup`, pass the new validator in as the second argument on creation.
@ -237,74 +242,73 @@ const heroForm = new FormGroup({
}, { validators: identityRevealedValidator }); }, { validators: identityRevealedValidator });
``` ```
The validator code is as follows: The validator code is as follows.
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-validator" header="shared/identity-revealed.directive.ts"></code-example> <code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-validator" header="shared/identity-revealed.directive.ts"></code-example>
The 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. The `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. The validator retrieves the child controls by calling the `FormGroup`'s [get](api/forms/AbstractControl#get) method, then compares the values of the `name` and `alterEgo` controls.
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. If the values do not match, the hero's identity remains secret, both are valid, and the validator returns null.
If they do match, the hero's identity is revealed and the validator must mark the form as invalid by returning an error object.
To provide better user experience, the template shows an appropriate error message when the form is invalid.
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" header="reactive/hero-form-template.component.html"></code-example> <code-example path="form-validation/src/app/reactive/hero-form-reactive.component.html" region="cross-validation-error-message" header="reactive/hero-form-template.component.html"></code-example>
Note that we check if: This `*ngIf` displays the error if the `FormGroup` has the cross validation error returned by the `identityRevealed` validator, but only if the user has finished [interacting with the form](#dirty-or-touched).
- 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.
### Adding to template driven forms ### Adding cross-validation 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).
For a template-driven form, you must create a directive to wrap the validator function.
You provide that directive as the validator using the [`NG_VALIDATORS` token](#adding-to-template-driven-forms "Read about providing validators"), as shown in the following example.
<code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-directive" header="shared/identity-revealed.directive.ts"></code-example> <code-example path="form-validation/src/app/shared/identity-revealed.directive.ts" region="cross-validation-directive" header="shared/identity-revealed.directive.ts"></code-example>
Next, we have to add the directive to the html template. Since the validator must be registered at the highest level in the form, we put the directive on the `form` tag. You must add the new directive to the HTML template.
Because the validator must be registered at the highest level in the form, the following template puts the directive on the `form` tag.
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-register-validator" header="template/hero-form-template.component.html"></code-example> <code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-register-validator" header="template/hero-form-template.component.html"></code-example>
To provide better user experience, we show an appropriate error message when the form is invalid. To provide better user experience, we show an appropriate error message when the form is invalid.
<code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-error-message" header="template/hero-form-template.component.html"></code-example> <code-example path="form-validation/src/app/template/hero-form-template.component.html" region="cross-validation-error-message" header="template/hero-form-template.component.html"></code-example>
Note that we check if: This is the same in both template-driven and reactive forms.
- the form 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.
This completes the cross validation example. We managed to: ## Creating asynchronous validators
- 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.
## Async Validation Asynchronous validators implement the `AsyncValidatorFn` and `AsyncValidator` interfaces.
This section shows how to create asynchronous validators. It assumes some basic knowledge of creating [custom validators](guide/form-validation#custom-validators). These are very similar to their synchronous counterparts, with the following differences.
### The Basics * The `validate()` functions must return a Promise or an observable,
Just like synchronous validators have the `ValidatorFn` and `Validator` interfaces, asynchronous validators have their own counterparts: `AsyncValidatorFn` and `AsyncValidator`. * The observable returned must be finite, meaning it must complete at some point.
To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
They are very similar with the only difference being: Asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful.
This check allows forms to avoid potentially expensive async validation processes (such as an HTTP request) if the more basic validation methods have already found invalid input.
* They must return a Promise or an Observable, After asynchronous validation begins, the form control enters a `pending` state. You can inspect the control's `pending` property and use it to give visual feedback about the ongoing validation operation.
* The observable returned must be finite, meaning it must complete at some point. To convert an infinite observable into a finite one, pipe the observable through a filtering operator such as `first`, `last`, `take`, or `takeUntil`.
It is important to note that the asynchronous validation happens after the synchronous validation, and is performed only if the synchronous validation is successful. This check allows forms to avoid potentially expensive async validation processes such as an HTTP request if more basic validation methods fail. A common UI pattern is to show a spinner while the async validation is being performed. The following example shows how to achieve this in a template-driven form.
After asynchronous validation begins, the form control enters a `pending` state. You can inspect the control's `pending` property and use it to give visual feedback about the ongoing validation.
A common UI pattern is to show a spinner while the async validation is being performed. The following example presents how to achieve this with template-driven forms:
```html ```html
<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator> <input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>
<app-spinner *ngIf="model.pending"></app-spinner> <app-spinner *ngIf="model.pending"></app-spinner>
``` ```
### Implementing Custom Async Validator ### Implementing a custom async validator
In the following section, validation is performed asynchronously to ensure that our heroes pick an alter ego that is not already taken. New heroes are constantly enlisting and old heroes are leaving the service. That means that we do not have the list of available alter egos ahead of time.
To validate the potential alter ego, we need to consult a central database of all currently enlisted heroes. The process is asynchronous, so we need a special validator for that. In the following example, an async validator ensures that heroes pick an alter ego that is not already taken.
New heroes are constantly enlisting and old heroes are leaving the service, so the list of available alter egos cannot be retrieved ahead of time.
To validate the potential alter ego entry, the validator must initiate an asynchronous operation to consult a central database of all currently enlisted heroes.
Let's start by creating the validator class. The following code create the validator class, `UniqueAlterEgoValidator`, which implements the `AsyncValidator` interface.
<code-example path="form-validation/src/app/shared/alter-ego.directive.ts" region="async-validator"></code-example> <code-example path="form-validation/src/app/shared/alter-ego.directive.ts" region="async-validator"></code-example>
As you can see, the `UniqueAlterEgoValidator` class implements the `AsyncValidator` interface. In the constructor, we inject the `HeroesService` that has the following interface: The constructor injects the `HeroesService`, which defines the following interface.
```typescript ```typescript
interface HeroesService { interface HeroesService {
@ -312,29 +316,37 @@ interface HeroesService {
} }
``` ```
In a real world application, the `HeroesService` is responsible for making an HTTP request to the hero database to check if the alter ego is available. From the validator's point of view, the actual implementation of the service is not important, so we can just code against the `HeroesService` interface. In a real world application, the `HeroesService` would be responsible for making an HTTP request to the hero database to check if the alter ego is available.
From the validator's point of view, the actual implementation of the service is not important, so the example can just code against the `HeroesService` interface.
As the validation begins, the `UniqueAlterEgoValidator` delegates to the `HeroesService` `isAlterEgoTaken()` method with the current control value. At this point the control is marked as `pending` and remains in this state until the observable chain returned from the `validate()` method completes. As the validation begins, the `UniqueAlterEgoValidator` delegates to the `HeroesService` `isAlterEgoTaken()` method with the current control value.
At this point the control is marked as `pending` and remains in this state until the observable chain returned from the `validate()` method completes.
The `isAlterEgoTaken()` method dispatches an HTTP request that checks if the alter ego is available, and returns `Observable<boolean>` as the result. We pipe the response through the `map` operator and transform it into a validation result. As always, we return `null` if the form is valid, and `ValidationErrors` if it is not. We make sure to handle any potential errors with the `catchError` operator. The `isAlterEgoTaken()` method dispatches an HTTP request that checks if the alter ego is available, and returns `Observable<boolean>` as the result.
The `validate()` method pipes the response through the `map` operator and transforms it into a validation result.
Here we decided that `isAlterEgoTaken()` error is treated as a successful validation, because failure to make a validation request does not necessarily mean that the alter ego is invalid. You could handle the error differently and return the `ValidationError` object instead. The method then, like any validator, returns `null` if the form is valid, and `ValidationErrors` if it is not.
This validator handles any potential errors with the `catchError` operator.
In this case, the validator treats the `isAlterEgoTaken()` error as a successful validation, because failure to make a validation request does not necessarily mean that the alter ego is invalid.
You could handle the error differently and return the `ValidationError` object instead.
After some time passes, the observable chain completes and the async validation is done. The `pending` flag is set to `false`, and the form validity is updated. After some time passes, the observable chain completes and the asynchronous validation is done.
The `pending` flag is set to `false`, and the form validity is updated.
### Note on performance ### Optimizing performance of async validators
By default, all validators are run after every form value change. With synchronous validators, this will not likely have a noticeable impact on application performance. However, it's common for async validators to perform some kind of HTTP request to validate the control. Dispatching an HTTP request after every keystroke could put a strain on the backend API, and should be avoided if possible. By default, all validators run after every form value change. With synchronous validators, this does not normally have a noticeable impact on application performance.
Async validators, however, commonly perform some kind of HTTP request to validate the control. Dispatching an HTTP request after every keystroke could put a strain on the backend API, and should be avoided if possible.
We can delay updating the form validity by changing the `updateOn` property from `change` (default) to `submit` or `blur`. You can delay updating the form validity by changing the `updateOn` property from `change` (default) to `submit` or `blur`.
With template-driven forms: With template-driven forms, set the property in the template.
```html ```html
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}"> <input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">
``` ```
With reactive forms: With reactive forms, set the property in the `FormControl` instance.
```typescript ```typescript
new FormControl('', {updateOn: 'blur'}); new FormControl('', {updateOn: 'blur'});

View File

@ -261,7 +261,7 @@
}, },
{ {
"url": "guide/form-validation", "url": "guide/form-validation",
"title": "Form Validation", "title": "Validate form input",
"tooltip": "Validate user's form entries." "tooltip": "Validate user's form entries."
}, },
{ {