Reactive Forms
Reactive forms is an Angular technique for creating forms in a reactive style. This guide explains reactive forms as you follow the steps to build a "Hero Detail Editor" form.
Try the
You can also run the
Introduction to Reactive Forms
Angular offers two form-building technologies: reactive forms and template-driven forms.
The two technologies belong to the @angular/forms
library
and share a common set of form control classes.
But they diverge markedly in philosophy, programming style, and technique.
They even have their own modules: the ReactiveFormsModule
and the FormsModule
.
Reactive forms
Angular reactive forms facilitate a reactive style of programming that favors explicit management of the data flowing between a non-UI data model (typically retrieved from a server) and a UI-oriented form model that retains the states and values of the HTML controls on screen. Reactive forms offer the ease of using reactive patterns, testing, and validation.
With reactive forms, you create a tree of Angular form control objects in the component class and bind them to native form control elements in the component template, using techniques described in this guide.
You create and manipulate form control objects directly in the component class. As the component class has immediate access to both the data model and the form control structure, you can push data model values into the form controls and pull user-changed values back out. The component can observe changes in form control state and react to those changes.
One advantage of working with form control objects directly is that value and validity updates are always synchronous and under your control. You won't encounter the timing issues that sometimes plague a template-driven form and reactive forms can be easier to unit test.
In keeping with the reactive paradigm, the component preserves the immutability of the data model, treating it as a pure source of original values. Rather than update the data model directly, the component extracts user changes and forwards them to an external component or service, which does something with them (such as saving them) and returns a new data model to the component that reflects the updated model state.
Using reactive form directives does not require you to follow all reactive priniciples, but it does facilitate the reactive programming approach should you choose to use it.
Template-driven forms
Template-driven forms, introduced in the Template guide, take a completely different approach.
You place HTML form controls (such as <input>
and <select>
) in the component template and
bind them to data model properties in the component, using directives
like ngModel
.
You don't create Angular form control objects. Angular directives
create them for you, using the information in your data bindings.
You don't push and pull data values. Angular handles that for you with ngModel
.
Angular updates the mutable data model with user changes as they happen.
For this reason, the ngModel
directive is not part of the ReactiveFormsModule.
While this means less code in the component class, template-driven forms are asynchronous which may complicate development in more advanced scenarios.
Async vs. sync
Reactive forms are synchronous while template-driven forms are asynchronous.
In reactive forms, you create the entire form control tree in code. You can immediately update a value or drill down through the descendants of the parent form because all controls are always available.
Template-driven forms delegate creation of their form controls to directives. To avoid "changed after checked" errors, these directives take more than one cycle to build the entire control tree. That means you must wait a tick before manipulating any of the controls from within the component class.
For example, if you inject the form control with a @ViewChild(NgForm)
query and examine it in the
ngAfterViewInit
lifecycle hook,
you'll discover that it has no children.
You must wait a tick, using setTimeout
, before you can
extract a value from a control, test its validity, or set it to a new value.
The asynchrony of template-driven forms also complicates unit testing.
You must wrap your test block in async()
or fakeAsync()
to
avoid looking for values in the form that aren't there yet.
With reactive forms, everything is available when you expect it to be.
Choosing reactive or template-driven forms
Reactive and template-driven forms are two different architectural paradigms, with their own strengths and weaknesses. Choose the approach that works best for you. You may decide to use both in the same application.
The rest of this page explores the reactive paradigm and concentrates exclusively on reactive forms techniques. For information on template-driven forms, see the Forms guide.
In the next section, you'll set up your project for the reactive form demo. Then you'll learn about the Angular form classes and how to use them in a reactive form.
Setup
Create a new project named angular-reactive-forms
:
ng new angular-reactive-forms
Create a data model
The focus of this guide is a reactive forms component that edits a hero.
You'll need a hero
class and some hero data.
Using the CLI, generate a new class named data-model
:
ng generate class data-model
And copy the following into data-model.ts
:
export class Hero {
id = 0;
name = '';
addresses: Address[];
}
export class Address {
street = '';
city = '';
state = '';
zip = '';
}
export const heroes: Hero[] = [
{
id: 1,
name: 'Whirlwind',
addresses: [
{street: '123 Main', city: 'Anywhere', state: 'CA', zip: '94801'},
{street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'},
]
},
{
id: 2,
name: 'Bombastic',
addresses: [
{street: '789 Elm', city: 'Smallville', state: 'OH', zip: '04501'},
]
},
{
id: 3,
name: 'Magneta',
addresses: [ ]
},
];
export const states = ['CA', 'MD', 'OH', 'VA'];
The file exports two classes and two constants. The Address
and Hero
classes define the application data model.
The heroes
and states
constants supply the test data.
Create a reactive forms component
Generate a new component named HeroDetail
:
ng generate component HeroDetail
And import:
import { FormControl } from '@angular/forms';
Next, update the HeroDetailComponent
class with a FormControl
.
FormControl
is a directive that allows you to create and manage
a FormControl
instance directly.
export class HeroDetailComponent1 {
name = new FormControl();
}
This creates a FormControl
called name
.
It will be bound in the template to an HTML <input>
element for the hero name.
A FormControl
constructor accepts three, optional arguments:
the initial data value, an array of validators, and an array of async validators.
This simple control doesn't have data or validators.
In real apps, most form controls have both. For in-depth information on
Validators
, see the Form Validation guide.
Create the template
Now update the component's template with the following markup.
<h2>Hero Detail</h2>
<h3><i>Just a FormControl</i></h3>
<label class="center-block">Name:
<input class="form-control" [formControl]="name">
</label>
To let Angular know that this is the input that you want to
associate to the name
FormControl
in the class,
you need [formControl]="name"
in the template on the <input>
.
Disregard the form-control
CSS class. It belongs to the
Bootstrap CSS library,
not Angular, and styles the form but in no way impacts the logic.
ReactiveFormsModule
Import the The HeroDetailComponent
template uses the formControlName
directive from the ReactiveFormsModule
.
Do the following two things in app.module.ts
:
- Use a JavaScript
import
statement to access theReactiveFormsModule
. - Add
ReactiveFormsModule
to theAppModule
'simports
list.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms'; // <-- #1 import module
import { AppComponent } from './app.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
@NgModule({
declarations: [
AppComponent,
HeroDetailComponent,
],
imports: [
BrowserModule,
ReactiveFormsModule // <-- #2 add to @NgModule imports
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
HeroDetailComponent
Display the Revise the AppComponent
template so it displays the HeroDetailComponent
.
<div class="container">
<h1>Reactive Forms</h1>
<app-hero-detail></app-hero-detail>
</div>
Essential form classes
This guide uses four fundamental classes to build a reactive form:
Class | Description |
---|---|
AbstractControl
|
|
FormControl
|
|
FormGroup
|
|
FormArray
|
|
Style the app
To use the bootstrap CSS classes that are in the template HTML of both the AppComponent
and the HeroDetailComponent
,
add the bootstrap
CSS stylesheet to the head of styles.css
:
@import url('https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css');
Now that everything is wired up, serve the app with:
ng serve
The browser should display something like this:
Add a FormGroup
Usually, if you have multiple FormControls
, you register
them within a parent FormGroup
.
To add a FormGroup
, add it to the imports section
of hero-detail.component.ts
:
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
In the class, wrap the FormControl
in a FormGroup
called heroForm
as follows:
export class HeroDetailComponent2 {
heroForm = new FormGroup ({
name: new FormControl()
});
}
Now that you've made changes in the class, they need to be reflected in the
template. Update hero-detail.component.html
by replacing it with the following.
<h2>Hero Detail</h2>
<h3><i>FormControl in a FormGroup</i></h3>
<form [formGroup]="heroForm">
<div class="form-group">
<label class="center-block">Name:
<input class="form-control" formControlName="name">
</label>
</div>
</form>
Notice that now the single <input>
is in a <form>
element.
formGroup
is a reactive form directive that takes an existing
FormGroup
instance and associates it with an HTML element.
In this case, it associates the FormGroup
you saved as
heroForm
with the <form>
element.
Because the class now has a FormGroup
, you must update the template
syntax for associating the <input>
with the corresponding
FormControl
in the component class.
Without a parent FormGroup
,
[formControl]="name"
worked earlier because that directive
can stand alone, that is, it works without being in a FormGroup
.
With a parent FormGroup
, the name
<input>
needs the syntax
formControlName=name
in order to be associated
with the correct FormControl
in the class. This syntax tells Angular to look for the parent
FormGroup
, in this case heroForm
, and then inside that group
to look for a FormControl
called name
.
Taking a look at the form model
When the user enters data into an <input>
, the value
goes into the form model.
To see the form model, add the following line after the
closing <form>
tag in the hero-detail.component.html
:
<p>Form value: {{ heroForm.value | json }}</p>
The heroForm.value
returns the form model.
Piping it through the JsonPipe
renders the model as JSON in the browser:
The initial name
property value is the empty string.
Type into the name <input>
and watch the keystrokes appear in the JSON.
In real life apps, forms get big fast.
FormBuilder
makes form development and maintenance easier.
FormBuilder
Introduction to The FormBuilder
class helps reduce repetition and
clutter by handling details of control creation for you.
To use FormBuilder
, import it into hero-detail.component.ts
. You can remove FormControl
:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
Use it to refactor the HeroDetailComponent
into something that's easier to read and write,
by following this plan:
- Explicitly declare the type of the
heroForm
property to beFormGroup
; you'll initialize it later. - Inject a
FormBuilder
into the constructor. - Add a new method that uses the
FormBuilder
to define theheroForm
; call itcreateForm()
. - Call
createForm()
in the constructor.
The revised HeroDetailComponent
looks like this:
export class HeroDetailComponent3 {
heroForm: FormGroup; // <--- heroForm is of type FormGroup
constructor(private fb: FormBuilder) { // <--- inject FormBuilder
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({
name: '', // <--- the FormControl called "name"
});
}
}
FormBuilder.group
is a factory method that creates a FormGroup
.
FormBuilder.group
takes an object whose keys and values are FormControl
names and their definitions.
In this example, the name
control is defined by its initial data value, an empty string.
Defining a group of controls in a single object makes your code more compact and readable because you don't have to write repeated new FormControl(...)
statements.
Validators.required
Though this guide doesn't go deeply into validations, here is one example that
demonstrates the simplicity of using Validators.required
in reactive forms.
First, import the Validators
symbol.
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
To make the name
FormControl
required, replace the name
property in the FormGroup
with an array.
The first item is the initial value for name
;
the second is the required validator, Validators.required
.
this.heroForm = this.fb.group({
name: ['', Validators.required ],
});
Reactive validators are simple, composable functions. Configuring validation is different in template-driven forms in that you must wrap validators in a directive.
Update the diagnostic message at the bottom of the template to display the form's validity status.
<p>Form value: {{ heroForm.value | json }}</p>
<p>Form status: {{ heroForm.status | json }}</p>
The browser displays the following:
Validators.required
is working. The status is INVALID
because the <input>
has no value.
Type into the <input>
to see the status change from INVALID
to VALID
.
In a real app, you'd replace the diagnosic message with a user-friendly experience.
Using Validators.required
is optional for the rest of the guide.
It remains in each of the following examples with the same configuration.
For more on validating Angular forms, see the Form Validation guide.
FormControl
s
More This section adds additional FormControl
s for the address, a super power, and a sidekick.
Additionally, the address has a state property. The user will select a state with a <select>
and you'll populate
the <option>
elements with states. So import states
from data-model.ts
.
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { states } from '../data-model';
Declare the states
property and add some address FormControls
to the heroForm
as follows.
export class HeroDetailComponent4 {
heroForm: FormGroup;
states = states;
constructor(private fb: FormBuilder) {
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({
name: ['', Validators.required ],
street: '',
city: '',
state: '',
zip: '',
power: '',
sidekick: ''
});
}
}
Then add corresponding markup in hero-detail.component.html
as follows.
<h2>Hero Detail</h2>
<h3><i>A FormGroup with multiple FormControls</i></h3>
<form [formGroup]="heroForm">
<div class="form-group">
<label class="center-block">Name:
<input class="form-control" formControlName="name">
</label>
</div>
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
<div class="form-group radio">
<h4>Super power:</h4>
<label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
<label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
<label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
</div>
<div class="checkbox">
<label class="center-block">
<input type="checkbox" formControlName="sidekick">I have a sidekick.
</label>
</div>
</form>
<p>Form value: {{ heroForm.value | json }}</p>
Note: Ignore the many mentions of form-group
,
form-control
, center-block
, and checkbox
in this markup.
Those are bootstrap CSS classes that Angular itself ignores.
Pay attention to the [formGroup]
and formControlName
attributes.
They are the Angular directives that bind the HTML controls to the
Angular FormGroup
and FormControl
properties in the component class.
The revised template includes more text <input>
elements, a <select>
for the state
, radio buttons for the power
,
and a <checkbox>
for the sidekick
.
You must bind the value property of the <option>
with [value]="state"
.
If you do not bind the value, the select shows the first option from the data model.
The component class defines control properties without regard for their representation in the template.
You define the state
, power
, and sidekick
controls the same way you defined the name
control.
You tie these controls to the template HTML elements in the same way,
specifying the FormControl
name with the formControlName
directive.
See the API reference for more information about radio buttons, selects, and checkboxes.
Nested FormGroups
To manage the size of the form more effectively, you can group some of the related FormControls
into a nested FormGroup
. For example, the street
, city
, state
, and zip
are ideal properties for an address FormGroup
.
Nesting groups and controls in this way allows you to
mirror the hierarchical structure of the data model
and helps track validation and state for related sets of controls.
You used the FormBuilder
to create one FormGroup
in this component called heroForm
.
Let that be the parent FormGroup
.
Use FormBuilder
again to create a child FormGroup
that encapsulates the address
controls;
assign the result to a new address
property of the parent FormGroup
.
export class HeroDetailComponent5 {
heroForm: FormGroup;
states = states;
constructor(private fb: FormBuilder) {
this.createForm();
}
createForm() {
this.heroForm = this.fb.group({ // <-- the parent FormGroup
name: ['', Validators.required ],
address: this.fb.group({ // <-- the child FormGroup
street: '',
city: '',
state: '',
zip: ''
}),
power: '',
sidekick: ''
});
}
}
When you change the structure of the form controls in the component class, you must make corresponding adjustments to the component template.
In hero-detail.component.html
, wrap the address-related FormControls
in a <div>
.
Add a formGroupName
directive to the div
and bind it to "address"
.
That's the property of the address
child FormGroup
within the parent FormGroup
called heroForm
. Leave the <div>
with the name
<input>
.
To make this change visually obvious, add an <h4>
header near the top with the text, Secret Lair.
The new address HTML looks like this:
<div formGroupName="address" class="well well-lg">
<h4>Secret Lair</h4>
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
</div>
After these changes, the JSON output in the browser shows the revised form model
with the nested address FormGroup
:
This shows that the template and the form model are talking to one another.
FormControl
Properties
Inspect You can inspect an individual FormControl
within a form by extracting it with the get()
method.
You can do this within the component class or display it on the
page by adding the following to the template,
immediately after the {{form.value | json}}
interpolation as follows:
<p>Name value: {{ heroForm.get('name').value }}</p>
To get the state of a FormControl
that’s inside a FormGroup
, use dot notation to traverse to the control.
<p>Street value: {{ heroForm.get('address.street').value}}</p>
Note: If you're coding along, remember to remove this reference to address.street
when you get to the section on FormArray
. In that section, you change the name of address in the component class and it will throw an error if you leave it in the template.
You can use this technique to display any property of a FormControl
such as one of the following:
Property | Description |
---|---|
myControl.value
|
the value of a |
myControl.status
|
the validity of a |
myControl.pristine
|
|
myControl.untouched
|
|
Read about other FormControl
properties in the
AbstractControl API reference.
One common reason for inspecting FormControl
properties is to
make sure the user entered valid values.
Read more about validating Angular forms in the
Form Validation guide.
The data model and the form model
At the moment, the form is displaying empty values.
The HeroDetailComponent
should display values of a hero,
possibly a hero retrieved from a remote server.
In this app, the HeroDetailComponent
gets its hero from a parent HeroListComponent
.
The hero
from the server is the data model.
The FormControl
structure is the form model.
The component must copy the hero values in the data model into the form model. There are two important implications:
-
The developer must understand how the properties of the data model map to the properties of the form model.
-
User changes flow from the DOM elements to the form model, not to the data model.
The form controls never update the data model.
The form and data model structures don't need to match exactly. You often present a subset of the data model on a particular screen. But it makes things easier if the shape of the form model is close to the shape of the data model.
In this HeroDetailComponent
, the two models are quite close.
Here are the definitions of Hero
and Address
in data-model.ts
:
export class Hero {
id = 0;
name = '';
addresses: Address[];
}
export class Address {
street = '';
city = '';
state = '';
zip = '';
}
Here, again, is the component's FormGroup
definition.
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group({
street: '',
city: '',
state: '',
zip: ''
}),
power: '',
sidekick: ''
});
There are two significant differences between these models:
-
The
Hero
has anid
. The form model does not because you generally don't show primary keys to users. -
The
Hero
has an array of addresses. This form model presents only one address, which is covered in the section onFormArray
below.
Keeping the two models close in shape facilitates copying the data model properties
to the form model with the patchValue()
and setValue()
methods in the next section.
First, refactor the address
FormGroup
definition as follows:
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group(new Address()), // <-- a FormGroup with a new address
power: '',
sidekick: ''
});
Also be sure to update the import
from data-model
so you can reference the Hero
and Address
classes:
import { Address, Hero, states } from '../data-model';
setValue()
and patchValue()
Populate the form model with Note: If you're coding along, this section is optional as the rest of the steps do not rely on it.
Previously, you created a control and initialized its value at the same time.
You can also initialize or reset the values later with the
setValue()
and patchValue()
methods.
setValue()
With setValue()
, you assign every form control value at once
by passing in a data object whose properties exactly match the form model behind the FormGroup
.
this.heroForm.setValue({
name: this.hero.name,
address: this.hero.addresses[0] || new Address()
});
The setValue()
method checks the data object thoroughly before assigning any form control values.
It will not accept a data object that doesn't match the FormGroup
structure or is
missing values for any control in the group. This way, it can return helpful
error messages if you have a typo or if you've nested controls incorrectly.
Conversely, patchValue()
will fail silently.
Notice that you can almost use the entire hero
as the argument to setValue()
because its shape is similar to the component's FormGroup
structure.
You can only show the hero's first address and you must account for the possibility that the hero
has no addresses at all, as in the conditional setting of the address
property in the data object argument:
address: this.hero.addresses[0] || new Address()
patchValue()
With patchValue()
, you can assign values to specific controls in a FormGroup
by supplying an object of key/value pairs for them.
This example sets only the form's name
control.
this.heroForm.patchValue({
name: this.hero.name
});
With patchValue()
you have more flexibility to cope with divergent data and form models.
But unlike setValue()
, patchValue()
cannot check for missing control
values and doesn't throw helpful errors.
HeroListComponent
and HeroService
Create the To demonstrate further reactive forms techniques, it is helpful to add more functionality to the example by adding a HeroListComponent
and a HeroService
.
The HeroDetailComponent
is a nested sub-component of the HeroListComponent
in a master/detail view. Together they look like this:
First, add a HeroListComponent
with the following command:
ng generate component HeroList
Give the HeroListComponent
the following contents:
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/finally';
import { Hero } from '../data-model';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {
heroes: Observable<Hero[]>;
isLoading = false;
selectedHero: Hero;
constructor(private heroService: HeroService) { }
ngOnInit() { this.getHeroes(); }
getHeroes() {
this.isLoading = true;
this.heroes = this.heroService.getHeroes()
// TODO: error handling
.finally(() => this.isLoading = false);
this.selectedHero = undefined;
}
select(hero: Hero) { this.selectedHero = hero; }
}
Next, add a HeroService
using the following command:
ng generate service Hero
Then, give it the following contents:
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import 'rxjs/add/operator/delay';
import { Hero, heroes } from './data-model';
@Injectable()
export class HeroService {
delayMs = 500;
// Fake server get; assume nothing can go wrong
getHeroes(): Observable<Hero[]> {
return of(heroes).delay(this.delayMs); // simulate latency with delay
}
// Fake server update; assume nothing can go wrong
updateHero(hero: Hero): Observable<Hero> {
const oldHero = heroes.find(h => h.id === hero.id);
const newHero = Object.assign(oldHero, hero); // Demo: mutate cached hero
return of(newHero).delay(this.delayMs); // simulate latency with delay
}
}
The HeroListComponent
uses an injected HeroService
to retrieve heroes from the server
and then presents those heroes to the user as a series of buttons.
The HeroService
emulates an HTTP service.
It returns an Observable
of heroes that resolves after a short delay,
both to simulate network latency and to indicate visually
the necessarily asynchronous nature of the application.
When the user clicks on a hero,
the component sets its selectedHero
property which
is bound to the hero
@Input()
property of the HeroDetailComponent
.
The HeroDetailComponent
detects the changed hero and resets its form
with that hero's data values.
A refresh button clears the hero list and the current selected hero before refetching the heroes.
Notice that hero-list.component.ts
imports Observable
and finally
while hero.service.ts
imports Observable
, of
,
and delay
from rxjs
.
The remaining HeroListComponent
and HeroService
implementation details are beyond the scope of this tutorial.
However, the techniques involved are covered elsewhere in the documentation, including the Tour of Heroes
here and here.
To use the HeroService
, import it into AppModule
and add it to the providers
array. To use the HeroListComponent
, import it, declare it, and export it:
// add JavaScript imports
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroService } from './hero.service';
@NgModule({
declarations: [
AppComponent,
HeroDetailComponent,
HeroListComponent // <--declare HeroListComponent
],
// ...
exports: [
AppComponent,
HeroDetailComponent,
HeroListComponent // <-- export HeroListComponent
],
providers: [ HeroService ], // <-- provide HeroService
Next, update the HeroListComponent
template with the following:
<h3 *ngIf="isLoading"><i>Loading heroes ... </i></h3>
<h3 *ngIf="!isLoading">Select a hero:</h3>
<nav>
<button (click)="getHeroes()" class="btn btn-primary">Refresh</button>
<a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>
<div *ngIf="selectedHero">
<hr>
<h2>Hero Detail</h2>
<h3>Editing: {{selectedHero.name}}</h3>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
</div>
These changes need to be reflected in the AppComponent
template. Replace the contents of app.component.html
with updated markup to use the HeroListComponent
, instead of the HeroDetailComponent
:
<div class="container">
<h1>Reactive Forms</h1>
<app-hero-list></app-hero-list>
</div>
Finally, add an @Input()
property to the HeroDetailComponent
so HeroDetailComponent
can receive the data from HeroListComponent
. Remember to add the Input
symbol to the @angular/core
import
statement in the list of JavaScript imports too.
@Input() hero: Hero;
Now you should be able to click on a button for a hero and a form renders.
ngOnChanges
)
When to set form model values (When to set form model values depends upon when the component gets the data model values.
The HeroListComponent
displays hero names to the user.
When the user clicks on a hero, the HeroListComponent
passes the selected hero into the HeroDetailComponent
by binding to its hero
@Input()
property.
<nav>
<a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>
<div *ngIf="selectedHero">
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
</div>
In this approach, the value of hero
in the HeroDetailComponent
changes
every time the user selects a new hero.
You can call setValue()
using the ngOnChanges
lifecycle hook, which Angular calls whenever the @Input()
hero
property changes.
Reset the form
First, import the OnChanges
symbol in hero-detail.component.ts
.
import { Component, Input, OnChanges } from '@angular/core';
Next, let Angular know that the HeroDetailComponent
implements OnChanges
:
export class HeroDetailComponent implements OnChanges {
Add the ngOnChanges
method to the class as follows:
ngOnChanges() {
this.rebuildForm();
}
Notice that it calls rebuildForm()
, which is a method where you
can set the values. You can name rebuildForm()
anything that makes sense to you. It isn't built into Angular, but is a method you create to effectively leverage the ngOnChanges
lifecycle hook.
rebuildForm() {
this.heroForm.reset({
name: this.hero.name,
address: this.hero.addresses[0] || new Address()
});
}
The rebuildForm()
method does two things; resets the hero's name and the address.
FormGroups
Use FormArray to present an array of A FormGroup
is a named object whose property values are FormControls
and other FormGroups
.
Sometimes you need to present an arbitrary number of controls or groups. For example, a hero may have zero, one, or any number of addresses.
The Hero.addresses
property is an array of Address
instances.
An address
FormGroup
can display one Address
.
An Angular FormArray
can display an array of address
FormGroups
.
To get access to the FormArray
class, import it into hero-detail.component.ts
:
import { Component, Input, OnChanges } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Address, Hero, states } from '../data-model';
To work with a FormArray
do the following:
-
Define the items in the array; that is,
FormControls
orFormGroups
. -
Initialize the array with items created from data in the data model.
-
Add and remove items as the user requires.
Define a FormArray
for Hero.addresses
and
let the user add or modify addresses.
You’ll need to redefine the form model in the HeroDetailComponent
createForm()
method,
which currently only displays the first hero address in an address
FormGroup
:
this.heroForm = this.fb.group({
name: ['', Validators.required ],
address: this.fb.group(new Address()), // <-- a FormGroup with a new address
power: '',
sidekick: ''
});
address
to secretLairs
From From the user's point of view, heroes don't have addresses.
Addresses are for mere mortals. Heroes have secret lairs!
Replace the address FormGroup
definition with a secretLairs
FormArray
definition:
this.heroForm = this.fb.group({
name: ['', Validators.required ],
secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray
power: '',
sidekick: ''
});
In hero-detail.component.html
change formArrayName="address"
to formArrayName="secretLairs"
.
<div formArrayName="secretLairs" class="well well-lg">
Changing the form control name from address
to secretLairs
underscores an important point:
the form model doesn't have to match the data model.
Obviously, there has to be a relationship between the two. But it can be anything that makes sense within the application domain.
Presentation requirements often differ from data requirements. The reactive forms approach both emphasizes and facilitates this distinction.
secretLairs
FormArray
Initialize the The default form displays a nameless hero with no addresses.
You need a method to populate (or repopulate) the secretLairs
with actual hero addresses whenever
the parent HeroListComponent
sets the HeroDetailComponent.hero
@Input()
property to a new Hero
.
The following setAddresses()
method replaces the secretLairs
FormArray
with a new FormArray
,
initialized by an array of hero address FormGroups
. Add this to the HeroDetailComponent
class:
setAddresses(addresses: Address[]) {
const addressFGs = addresses.map(address => this.fb.group(address));
const addressFormArray = this.fb.array(addressFGs);
this.heroForm.setControl('secretLairs', addressFormArray);
}
Notice that you replace the previous FormArray
with the
FormGroup.setControl()
method, not with setValue()
.
You're replacing a control, not the value of a control.
Notice also that the secretLairs
FormArray
contains FormGroups
, not Addresses
.
Next, call setAddresses()
from within rebuildForm()
:
rebuildForm() {
this.heroForm.reset({
name: this.hero.name
});
this.setAddresses(this.hero.addresses);
}
Get the FormArray
The HeroDetailComponent
should be able to display, add, and remove items from the secretLairs
FormArray
.
Use the FormGroup.get()
method to acquire a reference to that FormArray
.
Wrap the expression in a secretLairs
convenience property for clarity and re-use. Add the following to HeroDetailComponent
.
get secretLairs(): FormArray {
return this.heroForm.get('secretLairs') as FormArray;
};
Display the FormArray
The current HTML template displays a single address
FormGroup
.
Revise it to display zero, one, or more of the hero's address
FormGroups
.
This is mostly a matter of wrapping the previous template HTML for an address in a <div>
and
repeating that <div>
with *ngFor
.
There are three key points when writing the *ngFor
:
-
Add another wrapping
<div>
, around the<div>
with*ngFor
, and set itsformArrayName
directive to"secretLairs"
. This step establishes thesecretLairs
FormArray
as the context for form controls in the inner, repeated HTML template. -
The source of the repeated items is the
FormArray.controls
, not theFormArray
itself. Each control is anaddress
FormGroup
, exactly what the previous (now repeated) template HTML expected. -
Each repeated
FormGroup
needs a uniqueformGroupName
, which must be the index of theFormGroup
in theFormArray
. You'll re-use that index to compose a unique label for each address.
Here's the skeleton for the secret lairs section of the HTML template:
<div formArrayName="secretLairs" class="well well-lg">
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
<!-- The repeated address template -->
</div>
</div>
Here's the complete template for the secret lairs section. Add this to HeroDetailComponent
template, replacing the forGroupName=address
<div>
:
<div formArrayName="secretLairs" class="well well-lg">
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
<!-- The repeated address template -->
<h4>Address #{{i + 1}}</h4>
<div style="margin-left: 1em;">
<div class="form-group">
<label class="center-block">Street:
<input class="form-control" formControlName="street">
</label>
</div>
<div class="form-group">
<label class="center-block">City:
<input class="form-control" formControlName="city">
</label>
</div>
<div class="form-group">
<label class="center-block">State:
<select class="form-control" formControlName="state">
<option *ngFor="let state of states" [value]="state">{{state}}</option>
</select>
</label>
</div>
<div class="form-group">
<label class="center-block">Zip Code:
<input class="form-control" formControlName="zip">
</label>
</div>
</div>
<br>
<!-- End of the repeated address template -->
</div>
</div>
Add a new lair to the FormArray
Add an addLair()
method that gets the secretLairs
FormArray
and appends a new address
FormGroup
to it.
addLair() {
this.secretLairs.push(this.fb.group(new Address()));
}
Place a button on the form so the user can add a new secret lair and wire it to the component's addLair()
method. Put it just before the closing </div>
of the secretLairs
FormArray
.
<button (click)="addLair()" type="button">Add a Secret Lair</button>
Be sure to add the type="button"
attribute
because without an explicit type, the button type defaults to "submit".
When you later add a form submit action, every "submit" button triggers the submit action which
might do something like save the current changes.
You do not want to save changes when the user clicks the Add a Secret Lair button.
Try it!
Back in the browser, select the hero named "Magneta". "Magneta" doesn't have an address, as you can see in the diagnostic JSON at the bottom of the form.
Click the "Add a Secret Lair" button. A new address section appears. Well done!
Remove a lair
This example can add addresses but it can't remove them.
For extra credit, write a removeLair
method and wire it to a button on the repeating address HTML.
Observe control changes
Angular calls ngOnChanges()
when the user picks a hero in the parent HeroListComponent
.
Picking a hero changes the HeroDetailComponent.hero
@Input()
property.
Angular does not call ngOnChanges()
when the user modifies the hero's name
or secretLairs
.
Fortunately, you can learn about such changes by subscribing to one of the FormControl
properties
that raises a change event.
These are properties, such as valueChanges
, that return an RxJS Observable
.
You don't need to know much about RxJS Observable
to monitor form control values.
Add the following method to log changes to the value of the name
FormControl
.
nameChangeLog: string[] = [];
logNameChange() {
const nameControl = this.heroForm.get('name');
nameControl.valueChanges.forEach(
(value: string) => this.nameChangeLog.push(value)
);
}
Call it in the constructor, after createForm()
.
constructor(private fb: FormBuilder) {
this.createForm();
this.logNameChange();
}
The logNameChange()
method pushes name-change values into a nameChangeLog
array.
Display that array at the bottom of the component template with this *ngFor
binding:
<h4>Name change log</h4>
<div *ngFor="let name of nameChangeLog">{{name}}</div>
Return to the browser, select a hero; for example, Magneta, and start typing in the name
<input>
.
You should see a new name in the log after each keystroke.
When to use it
An interpolation binding is the easier way to display a name change.
Subscribing to an observable FormControl
property is handy for triggering
application logic within the component class.
Save form data
The HeroDetailComponent
captures user input but it doesn't do anything with it.
In a real app, you'd probably save those hero changes, revert unsaved changes, and resume editing.
After you implement both features in this section, the form will look like this:
Save
When the user submits the form,
the HeroDetailComponent
will pass an instance of the hero data model
to a save method on the injected HeroService
. Add the following to HeroDetailComponent
.
onSubmit() {
this.hero = this.prepareSaveHero();
this.heroService.updateHero(this.hero).subscribe(/* error handling */);
this.rebuildForm();
}
This original hero
had the pre-save values. The user's changes are still in the form model.
So you create a new hero
from a combination of original hero values (the hero.id
)
and deep copies of the changed form model values, using the prepareSaveHero()
helper.
prepareSaveHero(): Hero {
const formModel = this.heroForm.value;
// deep copy of form model lairs
const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
(address: Address) => Object.assign({}, address)
);
// return new `Hero` object containing a combination of original hero value(s)
// and deep copies of changed form model values
const saveHero: Hero = {
id: this.hero.id,
name: formModel.name as string,
// addresses: formModel.secretLairs // <-- bad!
addresses: secretLairsDeepCopy
};
return saveHero;
}
Make sure to import HeroService
and add it to the constructor:
import { HeroService } from '../hero.service';
constructor(
private fb: FormBuilder,
private heroService: HeroService) {
this.createForm();
this.logNameChange();
}
Address deep copy
Had you assigned the formModel.secretLairs
to saveHero.addresses
(see line commented out),
the addresses in saveHero.addresses
array would be the same objects
as the lairs in the formModel.secretLairs
.
A user's subsequent changes to a lair street would mutate an address street in the saveHero
.
The prepareSaveHero
method makes copies of the form model's secretLairs
objects so that can't happen.
Revert (cancel changes)
The user cancels changes and reverts the form to the original state by pressing the Revert button.
Reverting is easy. Simply re-execute the rebuildForm()
method that built the form model from the original, unchanged hero
data model.
revert() { this.rebuildForm(); }
Buttons
Add the "Save" and "Revert" buttons near the top of the component's template:
<form [formGroup]="heroForm" (ngSubmit)="onSubmit()">
<div style="margin-bottom: 1em">
<button type="submit"
[disabled]="heroForm.pristine" class="btn btn-success">Save</button>
<button type="button" (click)="revert()"
[disabled]="heroForm.pristine" class="btn btn-danger">Revert</button>
</div>
<!-- Hero Detail Controls -->
<div class="form-group radio">
<h4>Super power:</h4>
<label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
<label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
<label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
</div>
<div class="checkbox">
<label class="center-block">
<input type="checkbox" formControlName="sidekick">I have a sidekick.
</label>
</div>
</form>
The buttons are disabled until the user "dirties" the form by changing a value in any of its form controls (heroForm.dirty
).
Clicking a button of type "submit"
triggers the ngSubmit
event which calls the component's onSubmit
method.
Clicking the revert button triggers a call to the component's revert
method.
Users now can save or revert changes.
Try the
The key files of the final version are as follows:
<div class="container">
<h1>Reactive Forms</h1>
<app-hero-list></app-hero-list>
</div>
You can download the complete source for all steps in this guide
from the