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.
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.
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.
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.
因此,ngModel并不是ReactiveFormsModule模块的一部分。
For this reason, the ngModel directive is not part of the ReactiveFormsModule.
In reactive forms, you create the entire form control tree in code.
You can immediately update a value or drill down through the descendents 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.
Neither is "better".
They're 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 balance of this reactive forms guide 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.
The focus of this guide is a reactive forms component that edits a hero.
You'll need a hero class and some hero data.
Create a new data-model.ts file in the app directory and copy the content below into it.
src/app/data-model.ts
exportclassHero{
id =0;
name ='';
addresses:Address[];}exportclassAddress{
street ='';
city ='';
state ='';
zip ='';}exportconst 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:[]},];exportconst 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.
Next, create an exported HeroDetailComponent class with a FormControl.
FormControl is a directive that allows you to create and manage
a FormControl instance directly.
src/app/hero-detail.component.ts (excerpt)
exportclassHeroDetailComponent1{
name =newFormControl();}
To let Angular know that this is the input that you want to
associate to the nameFormControl in the class,
you need [formControl]="name" in the template on the <input>.
Disregard the form-controlCSS class. It belongs to the
Bootstrap CSS library,
not Angular.
It styles the form but in no way impacts the logic of the form.
AbstractControl
is the abstract base class for the three concrete form control classes:
FormControl, FormGroup, and FormArray.
It provides their common behaviors and properties, some of which are observable.
FormControl
tracks the value and validity status of an individual form control.
It corresponds to an HTML form control such as an input box or selector.
FormGroup
tracks the value and validity state of a group of AbstractControl instances.
The group's properties include its child controls.
The top-level form in your component is a FormGroup.
You used bootstrap CSS classes in the template HTML of both the AppComponent and the HeroDetailComponent.
Add the bootstrapCSS stylesheet to the head of index.html:
Usually, if you have multiple FormControls, you'll want to register
them within a parent FormGroup.
This is simple to do. To add a FormGroup, add it to the imports section
of hero-detail.component.ts:
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.
src/app/hero-detail.component.html
<h2>Hero Detail</h2><h3><i>FormControl in a FormGroup</i></h3><form [formGroup]="heroForm"novalidate><divclass="form-group"><labelclass="center-block">Name:
<inputclass="form-control"formControlName="name"></label></div></form>
Notice that now the single input is in a form element. The novalidate
attribute in the <form> element prevents the browser
from attempting native HTML validations.
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.
Disregard the form-groupCSS class. It belongs to the
Bootstrap CSS library,
not Angular.
Like the form-control class, it styles the form
but in no way impacts its logic.
表单看起来很棒,但是它能用吗?
当用户输入名字时,它的值去了哪里?
The form looks great. But does it work?
When the user enters a name, where does the value go?
The value goes into the form model that backs the group's FormControls.
To see the form model, add the following line after the
closing form tag in the hero-detail.component.html:
Use it now to refactor the HeroDetailComponent into something that's a little easier to read and write,
by following this plan:
明确把heroForm属性的类型声明为FormGroup,稍后我们会初始化它。
Explicitly declare the type of the heroForm property to be FormGroup; you'll initialize it later.
把FormBuilder注入到构造函数中。
Inject a FormBuilder into the constructor.
添加一个名叫createForm的新方法,它会用FormBuilder来定义heroForm。
Add a new method that uses the FormBuilder to define the heroForm; call it createForm.
在构造函数中调用createForm。
Call createForm in the constructor.
修改过的HeroDetailComponent代码如下:
The revised HeroDetailComponent looks like this:
src/app/hero-detail.component.ts (excerpt)
exportclassHeroDetailComponent3{
heroForm:FormGroup;// <--- heroForm is of type FormGroup
constructor(private fb:FormBuilder){// <--- inject FormBuilderthis.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 for a compact, readable style.
It beats writing an equivalent series of new FormControl(...) statements.
Though this guide doesn't go deeply into validations, here is one example that
demonstrates the simplicity of using Validators.required in reactive forms.
To make the nameFormControl 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.
Reactive validators are simple, composable functions.
Configuring validation is harder in template-driven forms where you must wrap validators in a directive.
修改模板底部的诊断信息,以显示表单的有效性状态。
Update the diagnostic message at the bottom of the template to display the form's validity status.
Validators.required is working. The status is INVALID because the input box has no value.
Type into the input box to see the status change from INVALID to VALID.
在真实的应用中,我们要把这些诊断信息替换成用户友好的信息。
In a real app, you'd replace the diagnosic message with a user-friendly experience.
The address has a state property. The user will select a state with a <select> box and you'll populate
the <option> elements with states. So import states from data-model.ts.
src/app/hero-detail.component.ts (excerpt)
import{Component}from'@angular/core';import{FormBuilder,FormGroup,Validators}from'@angular/forms';import{ states }from'./data-model';
声明states属性并往heroForm中添加一些表示住址的FormControl,代码如下:
Declare the states property and add some address FormControls to the heroForm as follows.
Reminder: 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 formGroupName 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 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,
specifiying the FormControl name with the formControlName directive.
This form is getting big and unwieldy. You can group some of the related FormControls
into a nested FormGroup. The street, city, state, and zip are properties
that would make a good addressFormGroup.
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.
src/app/hero-detail.component.ts (excerpt)
exportclassHeroDetailComponent5{
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:''});}}
我们已经修改了组件类中表单控件的结构,还必须对组件模板进行相应的调整。
You’ve changed 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.
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:
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.
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.
The form and data model structures need not 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.
在HeroDetailComponent中,这两个模型是非常接近的。
In this HeroDetailComponent, the two models are quite close.
回忆一下data-model.ts中的Hero定义:
Recall the definition of Hero in data-model.ts:
src/app/data-model.ts (classes)
exportclassHero{
id =0;
name ='';
addresses:Address[];}exportclassAddress{
street ='';
city ='';
state ='';
zip ='';}
这里又是组件的FormGroup定义。
Here, again, is the component's FormGroup definition.
Nonetheless, the two models are pretty close in shape and you'll see in a moment how this alignment facilitates copying the data model properties
to the form model with the patchValue and setValue methods.
花一点时间来重构一下address这个FormGroup定义,来让它更简洁清晰,代码如下:
Take a moment to refactor the addressFormGroup definition for brevity and clarity as follows:
this.heroForm =this.fb.group({
name:['',Validators.required ],
address:this.fb.group(newAddress()),// <-- a FormGroup with a new address
power:'',
sidekick:''});
为了确保从data-model中导入,我们可以引用Hero和Address类:
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和patchValue来操纵表单模型
Populate the form model with setValue and patchValue
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.
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.
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.
patchValue will fail silently.
而setValue会捕获错误,并清晰的报告它。
On the other hand,setValue will catch
the error and report it clearly.
You can only show the hero's first address and you must account for the possibility that the hero has no addresses at all.
This explains the conditional setting of the address property in the data object argument:
With patchValue, you can assign values to specific controls in a FormGroup
by supplying an object of key/value pairs for just the controls of interest.
With patchValue you have more flexibility to cope with wildly divergent data and form models.
But unlike setValue, patchValue cannot check for missing control
values and does not throw helpful errors.
The HeroDetailComponent in this reactive forms sample is nested within a master/detailHeroListComponent (discussed below).
The HeroListComponent displays hero names to the user.
When the user clicks on a hero, the list component passes the selected hero into the HeroDetailComponent
by binding to its hero input property.
hero-list.component.html (simplified)
<nav><a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a></nav><div *ngIf="selectedHero"><hero-detail [hero]="selectedHero"></hero-detail></div>
In this approach, the value of hero in the HeroDetailComponent changes
every time the user selects a new hero.
You should call setValue in the ngOnChanges
hook, which Angular calls whenever the input hero property changes
as the following steps demonstrate.
首先,在hero-detail.component.ts中导入OnChanges和Input符号。
First, import the OnChanges and Input symbols in hero-detail.component.ts.
You should reset the form when the hero changes so that
control values from the previous hero are cleared and
status flags are restored to the pristine state.
You could call reset at the top of ngOnChanges like this.
The reset method has an optional state value so you can reset the flags and the control values at the same.
Internally, reset passes the argument to setValue.
A little refactoring and ngOnChanges becomes this:
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 re-sets its form
with that hero's data values.
"刷新"按钮清除英雄列表和当前选中的英雄,然后重新获取英雄列表。
A "Refresh" button clears the hero list and the current selected hero before refetching the heroes.
The remaining HeroListComponent and HeroService implementation details are not relevant to understanding reactive forms.
The techniques involved are covered elsewhere in the documentation, including the Tour of Heroeshere and here.
If you're coding along with the steps in this reactive forms tutorial,
create the pertinent files based on the
source code displayed below.
Notice that hero-list.component.ts imports Observable and finally while hero.service.ts imports Observable, of,
and delay from rxjs.
Then return here to learn about form array properties.
The Hero.addresses property is an array of Address instances.
An addressFormGroup can display one Address.
An Angular FormArray can display an array of addressFormGroups.
要访问FormArray类,请先把它导入hero-detail.component.ts中:
To get access to the FormArray class, import it into hero-detail.component.ts:
src/app/hero-detail.component.ts (excerpt)
import{Component,Input,OnChanges}from'@angular/core';import{FormArray,FormBuilder,FormGroup,Validators}from'@angular/forms';import{Address,Hero, states }from'./data-model';
要想使用FormArray,我们要这么做:
To work with a FormArray you do the following:
在数组中定义条目(FormControl或FormGroup)。
Define the items (FormControls or FormGroups) in the array.
把这个数组初始化微一组从数据模型中的数据创建的条目。
Initialize the array with items created from data in the data model.
You’ll need to redefine the form model in the HeroDetailComponent constructor,
which currently only displays the first hero address in an addressFormGroup.
this.heroForm =this.fb.group({
name:['',Validators.required ],
address:this.fb.group(newAddress()),// <-- a FormGroup with a new address
power:'',
sidekick:''});
From the user's point of view, heroes don't have addresses.
Addresses are for mere mortals. Heroes have secret lairs!
Replace the addressFormGroup definition with a secretLairsFormArray definition:
this.heroForm =this.fb.group({
name:['',Validators.required ],
secretLairs:this.fb.array([]),// <-- secretLairs as an empty FormArray
power:'',
sidekick:''});
You need a method to populate (or repopulate) the secretLairs with actual hero addresses whenever
the parent HeroListComponent sets the HeroListComponent.hero input property to a new Hero.
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.
还要注意,secretLairs数组中包含的是**FormGroup,而不是Address。
Notice also that the secretLairsFormArray contains FormGroups, not Addresses.
获取FormArray
Get the FormArray
HeroDetailComponent应该能从secretLairs中显示、添加和删除条目。
The HeroDetailComponent should be able to display, add, and remove items from the secretLairsFormArray.
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 another wrapping <div>, around the <div> with *ngFor, and
set its formArrayName directive to "secretLairs".
This step establishes the secretLairsFormArray as the context for form controls in the inner, repeated HTML template.
The source of the repeated items is the FormArray.controls, not the FormArray itself.
Each control is an addressFormGroup, exactly what the previous (now repeated) template HTML expected.
Each repeated FormGroup needs a unique formGroupName which must be the index of the FormGroup in the FormArray.
You'll re-use that index to compose a unique label for each address.
下面是HTML模板中秘密小屋部分的代码骨架:
Here's the skeleton for the secret lairs section of the HTML template:
src/app/hero-detail.component.html (*ngFor)
<divformArrayName="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:
<button (click)="addLair()"type="button">Add a Secret Lair</button>
务必确保添加了type="button"属性。
事实上,我们应该总是指定按钮的type。
如果不明确指定类型,按钮的默认类型就是“submit”(提交)。
当我们稍后添加了表单提交的动作时,每个“submit”按钮都是触发一次提交操作,而它将可能会做一些处理,比如保存当前的修改。
我们显然不会希望每当用户点击“Add a Secret Lair”按钮时就保存一次。
Be sure to add the type="button" attribute.
In fact, you should always specify a button's type.
Without an explict 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.
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.
点击“Add a Secret Lair”按钮,一个新的地址区就出现了,干得好!
Click the "Add a Secret Lair" button.
A new address section appears. Well done!
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.
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 secret lairs.
Fortunately, you can learn about such changes by subscribing to one of the form control 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.
添加下列方法,以监听姓名这个FormControl中值的变化。
Add the following method to log changes to the value of the nameFormControl.
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:
Return to the browser, select a hero (e.g, "Magneta"), and start typing in the name input box.
You should see a new name in the log after each keystroke.
An interpolation binding is the easier way to display a name change.
Subscribing to an observable form control property is handy for triggering
application logic within the component class.
The HeroDetailComponent captures user input but it doesn't do anything with it.
In a real app, you'd probably save those hero changes.
In a real app, you'd also be able to revert unsaved changes and resume editing.
After you implement both features in this section, the form will look like this:
In this sample application, 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.
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 lairsconst 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 valuesconst saveHero:Hero={
id:this.hero.id,
name: formModel.name asstring,// addresses: formModel.secretLairs // <-- bad!
addresses: secretLairsDeepCopy
};return saveHero;}
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.
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.