docs(aio): add async validation chapter (#25189)

Closes #22881

PR Close #25189
This commit is contained in:
Tomasz Kula 2018-07-29 11:47:59 +02:00 committed by Ben Lesh
parent 409860a4da
commit c8c1aa7fc0
9 changed files with 293 additions and 43 deletions

View File

@ -16,6 +16,7 @@ describe('Form Validation Tests', function () {
tests('Template-Driven Form');
bobTests();
asyncValidationTests();
crossValidationTests();
});
@ -26,6 +27,7 @@ describe('Form Validation Tests', function () {
tests('Reactive Form');
bobTests();
asyncValidationTests();
crossValidationTests();
});
});
@ -45,6 +47,7 @@ let page: {
errorMessages: ElementArrayFinder,
heroFormButtons: ElementArrayFinder,
heroSubmitted: ElementFinder,
alterEgoErrors: ElementFinder,
crossValidationErrorMessage: ElementFinder,
};
@ -63,6 +66,7 @@ function getPage(sectionTag: string) {
errorMessages: section.all(by.css('div.alert')),
heroFormButtons: buttons,
heroSubmitted: section.element(by.css('.submitted-message')),
alterEgoErrors: section.element(by.css('.alter-ego-errors')),
crossValidationErrorMessage: section.element(by.css('.cross-validation-error-message')),
};
}
@ -156,6 +160,16 @@ function expectFormIsInvalid() {
expect(page.form.getAttribute('class')).toMatch('ng-invalid');
}
function triggerAlterEgoValidation() {
// alterEgo has updateOn set to 'blur', click outside of the input to trigger the blur event
element(by.css('app-root')).click()
}
function waitForAlterEgoValidation() {
// alterEgo async validation will be performed in 400ms
browser.sleep(400);
}
function bobTests() {
const emsg = 'Name cannot be Bob.';
@ -177,6 +191,32 @@ function bobTests() {
});
}
function asyncValidationTests() {
const emsg = 'Alter ego is already taken.';
it(`should produce "${emsg}" error after setting alterEgo to Eric`, function () {
page.alterEgoInput.clear();
page.alterEgoInput.sendKeys('Eric');
triggerAlterEgoValidation();
waitForAlterEgoValidation();
expectFormIsInvalid();
expect(page.alterEgoErrors.getText()).toBe(emsg);
});
it('should be ok again with different values', function () {
page.alterEgoInput.clear();
page.alterEgoInput.sendKeys('John');
triggerAlterEgoValidation();
waitForAlterEgoValidation();
expectFormIsValid();
expect(page.alterEgoErrors.isPresent()).toBe(false);
});
}
function crossValidationTests() {
const emsg = 'Name cannot match alter ego.';
@ -187,6 +227,9 @@ function crossValidationTests() {
page.alterEgoInput.clear();
page.alterEgoInput.sendKeys('Batman');
triggerAlterEgoValidation();
waitForAlterEgoValidation();
expectFormIsInvalid();
expect(page.crossValidationErrorMessage.getText()).toBe(emsg);
});
@ -198,6 +241,9 @@ function crossValidationTests() {
page.alterEgoInput.clear();
page.alterEgoInput.sendKeys('Superman');
triggerAlterEgoValidation();
waitForAlterEgoValidation();
expectFormIsValid();
expect(page.crossValidationErrorMessage.isPresent()).toBe(false);
});

View File

@ -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 ]
})

View File

@ -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
}

View File

@ -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 -->

View File

@ -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) { }
}

View File

@ -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

View File

@ -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));
}
}

View File

@ -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 -->

View File

@ -6,12 +6,12 @@
Improve overall data quality by validating user input for accuracy and completeness.
This page shows how to validate user input in the UI and display useful validation messages
using both reactive and template-driven forms. It assumes some basic knowledge of the two
using both reactive and template-driven forms. It assumes some basic knowledge of the two
forms modules.
<div class="alert is-helpful">
If you're new to forms, start by reviewing the [Forms](guide/forms) and
If you're new to forms, start by reviewing the [Forms](guide/forms) and
[Reactive Forms](guide/reactive-forms) guides.
</div>
@ -19,11 +19,11 @@ If you're new to forms, start by reviewing the [Forms](guide/forms) and
## Template-driven validation
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).
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).
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.
You can then inspect the control's state by exporting `ngModel` to a local template variable.
@ -36,12 +36,12 @@ The following example exports `NgModel` into a variable called `name`:
Note the following:
* The `<input>` element carries the HTML validation attributes: `required` and `minlength`. It
also carries a custom validator directive, `forbiddenName`. For more
* The `<input>` element carries the HTML validation attributes: `required` and `minlength`. It
also carries a custom validator directive, `forbiddenName`. For more
information, see [Custom validators](guide/form-validation#custom-validators) section.
* `#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)
* `#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)
API reference.
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
@ -49,7 +49,7 @@ 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.
There are messages for `required`, `minlength`, and `forbiddenName`.
<div class="alert is-helpful">
@ -58,8 +58,8 @@ There are messages for `required`, `minlength`, and `forbiddenName`.
#### Why check _dirty_ and _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,
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>
@ -70,24 +70,24 @@ In a reactive form, the source of truth is the component class. Instead of addin
### Validator functions
There are two types of validator functions: sync validators and async validators.
There are two types of validator functions: sync validators and async validators.
* **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`.
* **Async validators**: functions that take a control instance and return a Promise
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`.
* **Async validators**: functions that take a control instance and return a Promise
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`.
Note: for performance reasons, Angular only runs async validators if all sync validators pass. Each must complete before errors are set.
### Built-in validators
You can choose to [write your own validator functions](guide/form-validation#custom-validators), or you can use some of
Angular's built-in validators.
You can choose to [write your own validator functions](guide/form-validation#custom-validators), or you can use some of
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.
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:
{@a reactive-component-class}
@ -98,31 +98,31 @@ built-in validators&mdash;this time, in function form. See below:
Note that:
* 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.
* As these validators are all sync validators, you pass them in as the second argument.
* 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
* 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.
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" title="reactive/hero-form-reactive.component.html (name with error msg)" linenums="false">
</code-example>
Key takeaways:
* The form no longer exports any directives, and instead uses the `name` getter defined in
* The form no longer exports any directives, and instead uses the `name` getter defined in
the component class.
* The `required` attribute is still present. While it's not necessary for validation purposes,
* 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.
## 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.
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.
Consider the `forbiddenNameValidator` function from previous
[examples](guide/form-validation#reactive-component-class) in
[examples](guide/form-validation#reactive-component-class) in
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" title="shared/forbidden-name.directive.ts (forbiddenNameValidator)" linenums="false">
@ -145,7 +145,7 @@ at which point the form uses the last value emitted for validation.
### Adding to reactive forms
In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly
In reactive forms, custom validators are fairly simple to add. All you have to do is pass the function directly
to the `FormControl`.
<code-example path="form-validation/src/app/reactive/hero-form-reactive.component.1.ts" region="custom-validator" title="reactive/hero-form-reactive.component.ts (validator functions)" linenums="false">
@ -153,7 +153,7 @@ to the `FormControl`.
### 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
In template-driven forms, you don't have direct access to the `FormControl` instance, so you can't pass the
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`.
@ -164,8 +164,8 @@ with the `NG_VALIDATORS` provider, a provider with an extensible collection of v
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive-providers" title="shared/forbidden-name.directive.ts (providers)" linenums="false">
</code-example>
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
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
comes together:
<code-example path="form-validation/src/app/shared/forbidden-name.directive.ts" region="directive" title="shared/forbidden-name.directive.ts (directive)">
@ -201,14 +201,14 @@ Like in AngularJS, Angular automatically mirrors many control properties onto th
* `.ng-untouched`
* `.ng-touched`
The hero form uses the `.ng-valid` and `.ng-invalid` classes to
The hero form uses the `.ng-valid` and `.ng-invalid` classes to
set the color of each form control's border.
<code-example path="form-validation/src/assets/forms.css" title="forms.css (status classes)">
</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">
@ -217,7 +217,7 @@ If you haven't created custom validators before, start by reviewing the [custom
</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.
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
@ -250,7 +250,7 @@ The validator code is as follows:
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.
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 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.
@ -259,7 +259,7 @@ Next, to provide better user experience, we show an appropriate error message wh
</code-example>
Note that we check if:
- the `FormGroup` has the cross validation error returned by the `identityRevealed` validator,
- 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
@ -277,11 +277,78 @@ To provide better user experience, we show an appropriate error message when the
</code-example>
Note that we check if:
- the form has the cross validation error returned by the `identityRevealed` validator,
- 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:
- validate the form based on the values of two sibling controls,
- 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
This section shows how to create asynchronous validators. It assumes some basic knowledge of creating [custom validators](guide/form-validation#custom-validators).
### The Basics
Just like synchronous validators have the `ValidatorFn` and `Validator` interfaces, asynchronous validators have their own counterparts: `AsyncValidatorFn` and `AsyncValidator`.
They are very similar with the only difference being:
* They must return a Promise or an Observable,
* 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.
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
<input [(ngModel)}="name" #model="ngModel" appSomeAsyncValidator>
<app-spinner *ngIf="model.pending"></app-spinner>
```
### Implementing 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.
Let's start by creating the validator class.
<code-example path="form-validation/src/app/shared/alter-ego.directive.ts" region="async-validator" linenums="false"></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:
```typescript
interface HeroesService {
isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
}
```
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.
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.
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.
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.
### Note on performance
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.
We can delay updating the form validity by changing the `updateOn` property from `change` (default) to `submit` or `blur`.
With template-driven forms:
```html
<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">
```
With reactive forms:
```typescript
new FormControl('', {updateOn: 'blur'});
```
**You can run the <live-example></live-example> to see the complete reactive and template-driven example code.**