This cookbook shows how to validate user input in the UI and display useful validation messages
using first the template-driven forms and then the reactive forms approach.
You add Angular form directives (mostly directives beginning ng...) to help
Angular construct a corresponding internal control model that implements form functionality.
In template-drive forms, the control model is implicit in the template.
To validate user input, you add HTML validation attributes
to the elements. Angular interprets those as well, adding validator functions to the control model.
Angular exposes information about the state of the controls including
whether the user has "touched" the control or made changes and if the control values are valid.
In this first template validation example,
notice the HTML that reads the control state and updates the display appropriately.
Here's an excerpt from the template HTML for a single input control bound to the hero name:
<labelfor="name">Name</label><inputtype="text"id="name"class="form-control"requiredminlength="4"maxlength="24"name="name" [(ngModel)]="hero.name"
#name="ngModel"><div *ngIf="name.errors && (name.dirty || name.touched)"class="alert alert-danger"><div [hidden]="!name.errors.required">
Name is required
</div><div [hidden]="!name.errors.minlength">
Name must be at least 4 characters long.
</div><div [hidden]="!name.errors.maxlength">
Name cannot be more than 24 characters long.
</div></div>
The name attribute of the input is set to "name" so Angular can track this input element and associate it
with an Angular form control called name in its internal control model.
我们使用[(ngModel)]指令,将输入框双向数据绑定到hero.name属性。
The [(ngModel)] directive allows two-way data binding between the input box to the hero.name property.
The template variable (#name) has the value "ngModel" (always ngModel).
This gives you a reference to the Angular NgModel directive
associated with this control that you can use in the template
to check for control states such as valid and dirty.
The app shouldn't show errors for a new hero before the user has had a chance to edit the value.
The checks for dirty and touched prevent premature display of errors.
There's a new attribute, forbiddenName, that is actually a custom validation directive.
It invalidates the control if the user enters "bob" in the name <input>(try it).
See the custom validation section later in this cookbook for more information
on custom validation directives.
模板变量#name消失了,因为我们不再需要为这个元素引用Angular控制器。
The #name template variable is gone because the app no longer refers to the Angular control for this element.
绑定到新的formErrors.name属性,就可以处理所有名字验证错误信息了。
Binding to the new formErrors.name property is sufficent to display all name validation error messages.
The original component code for Template 1 stayed the same; however,
Template 2 requires some changes in the component. This section covers the code
necessary in Template 2's component class to acquire the Angular
form control and compose error messages.
第一步是获取Angular通过查询模板而生成的表单控制器。
The first step is to acquire the form control that Angular created from the template by querying for it.
回头看组件模板顶部,我们在<form>元素中设置#heroForm模板变量:
Look back at the top of the component template at the
#heroForm template variable in the <form> element:
The heroForm variable is a reference to the control model that Angular derived from the template.
Tell Angular to inject that model into the component class's currentForm property using a @ViewChild query:
The heroForm object changes several times during the life of the component, most notably when you add a new hero.
Periodically inspecting it reveals these changes.
Angular calls the ngAfterViewCheckedlifecycle hook method
when anything changes in the view.
That's the right time to see if there's a new heroForm object.
When there is a new heroForm model, formChanged() subscribes to its valueChangesObservable property.
The onValueChanged handler looks for validation errors after every keystroke.
The onValueChanged handler interprets user data entry.
The data object passed into the handler contains the current element values.
The handler ignores them. Instead, it iterates over the fields of the component's formErrors object.
The formErrors is a dictionary of the hero fields that have validation rules and their current error messages.
Only two hero properties have validation rules, name and power.
The messages are empty strings when the hero data are valid.
对于每个字段,这个onValueChanged处理器会做这些:
For each field, the onValueChanged handler does the following:
清除以前的错误信息(如果有的话)
Clears the prior error message, if any.
获取控件对应的Angular表单控制器
Acquires the field's corresponding Angular form control.
If such a control exists and it's been changed ("dirty")
and it's invalid, the handler composes a consolidated error message for all of the control's errors.
很显然,我们需要一些错误消息,每个验证的属性都需要一套,每个验证规则需要一条消息:
Next, the component needs some error messages of course—a set for each validated property with
one message per validation rule:
validationMessages ={'name':{'required':'Name is required.','minlength':'Name must be at least 4 characters long.','maxlength':'Name cannot be more than 24 characters long.','forbiddenName':'Someone named "Bob" cannot be a hero.'},'power':{'required':'Power is required.'}};
现在,每次用户作出变化时,onValueChanged处理器检查验证错误并按情况发出错误消息。
Now every time the user makes a change, the onValueChanged handler checks for validation errors and produces messages accordingly.
Clearly the template got substantially smaller while the component code got substantially larger.
It's not easy to see the benefit when there are just three fields and only two of them have validation rules.
Consider what happens as the number of validated
fields and rules increases.
In general, HTML is harder to read and maintain than code.
The initial template was already large and threatening to get rapidly worse
with the addition of more validation message <div> elements.
After moving the validation messaging to the component,
the template grows more slowly and proportionally.
Each field has approximately the same number of lines no matter its number of validation rules.
The component also grows proportionally, at the rate of one line per validated field
and one line per validation message.
Now that the messages are in code, you have more flexibility and can compose messages more efficiently.
You can refactor the messages out of the component, perhaps to a service class that retrieves them from the server.
In short, there are more opportunities to improve message handling now that text and logic have moved from template to code.
Angular has two different forms modules—FormsModule and
ReactiveFormsModule—that correspond with the
two approaches to form development. Both modules come
from the same @angular/forms library package.
In the template-driven approach, you markup the template with form elements, validation attributes,
and ng... directives from the Angular FormsModule.
At runtime, Angular interprets the template and derives its form control model.
Reactive Forms takes a different approach.
You create the form control model in code. You write the template with form elements
and form... directives from the Angular ReactiveFormsModule.
At runtime, Angular binds the template elements to your control model based on your instructions.
这个方法需要做一些额外的工作。你必须编写并管理控制器模型*。
This approach requires a bit more effort. You have to write the control model and manage it.
这可以让你:
This allows you to do the following:
随时添加、修改和删除验证函数
Add, change, and remove validation functions on the fly.
在组件内动态操纵控制器模型
Manipulate the control model dynamically from within the component.
The reactive forms classes and directives come from the Angular ReactiveFormsModule, not the FormsModule.
The application module for the reactive forms feature in this sample looks like this:
The reactive forms feature module and component are in the src/app/reactive folder.
Focus on the HeroFormReactiveComponent there, starting with its template.
Begin by changing the <form> tag so that it binds the Angular formGroup directive in the template
to the heroForm property in the component class.
The heroForm is the control model that the component class builds and maintains.
Next, modify the template HTML elements to match the reactive forms style.
Here is the "name" portion of the template again, revised for reactive forms and compared with the template-driven version:
A future version of reactive forms will add the required HTML validation attribute to the DOM element
(and perhaps the aria-required attribute) when the control has the required validator function.
The two-way [(ngModel)] binding is gone.
The reactive approach does not use data binding to move data into and out of the form controls.
That's all in code.
不适用表单数据绑定是响应式模式的原则,而非技术限制。
The retreat from data binding is a principle of the reactive paradigm rather than a technical limitation.
组件类
Component class
组件类现在负责定义和管理表单控制器模型。
The component class is now responsible for defining and managing the form control model.
Angular no longer derives the control model from the template so you can no longer query for it.
You can create the Angular form control model explicitly with the help of the FormBuilderclass.
下面是负责该进程的代码部分,与被它取代的模板驱动代码相比:
Here's the section of code devoted to that process, paired with the template-driven code it replaces:
heroForm:FormGroup;
constructor(private fb:FormBuilder){}
ngOnInit():void{
this.buildForm();
}
buildForm():void{
this.heroForm =this.fb.group({
'name':[this.hero.name,[
Validators.required,
Validators.minLength(4),
Validators.maxLength(24),
forbiddenNameValidator(/bob/i)
]
],
'alterEgo':[this.hero.alterEgo],
'power':[this.hero.power,Validators.required]
});
this.heroForm.valueChanges
.subscribe(data =>this.onValueChanged(data));
this.onValueChanged();// (re)set validation messages now
The buildForm method uses the FormBuilder, fb, to declare the form control model.
Then it attaches the same onValueChanged handler (there's a one line difference)
to the form's valueChanges event and calls it immediately
to set error messages for the new control model.
FormBuilder声明
FormBuilder declaration
FormBuilder声明对象指定了本例英雄表单的三个控制器。
The FormBuilder declaration object specifies the three controls of the sample's hero form.
Each control spec is a control name with an array value.
The first array element is the current value of the corresponding hero field.
The optional second value is a validator function or an array of validator functions.
Most of the validator functions are stock validators provided by Angular as static methods of the Validators class.
Angular has stock validators that correspond to the standard HTML validation attributes.
In two-way data binding, the user's changes flow automatically from the controls back to the data model properties.
Reactive forms do not use data binding to update data model properties.
The developer decides when and how to update the data model from control values.
本例更新模型两次:
This sample updates the model twice:
当用户提交标单时
When the user submits the form.
当用户添加新英雄时
When the user adds a new hero.
onSubmit()方法直接使用表单的值得合集来替换hero对象:
The onSubmit() method simply replaces the hero object with the combined values of the form:
Then it calls buildForm() again which replaces the previous heroForm control model with a new one.
The <form> tag's [formGroup] binding refreshes the page with the new control model.
下面是完整的响应式表单的组件文件,与两个模板驱动组件文件对比:
Here's the complete reactive component file, compared to the two template-driven component files.
This cookbook sample has a custom forbiddenNamevalidator() function that's applied to both the
template-driven and the reactive form controls. It's in the src/app/shared folder
and declared in the SharedModule.
/** A hero's name can't match the given regular expression */exportfunction forbiddenNameValidator(nameRe:RegExp):ValidatorFn{return(control:AbstractControl):{[key:string]: any}=>{const name = control.value;constno= nameRe.test(name);returnno?{'forbiddenName':{name}}:null;};}
该函数其实是一个工厂函数,接受一个正则表达式,用来检测指定的禁止的名字,并返回验证器函数。
The function is actually a factory that takes a regular expression to detect a specific forbidden name
and returns a validator function.
In this sample, the forbidden name is "bob";
the validator rejects any hero name containing "bob".
Elsewhere it could reject "alice" or any name that the configuring regular expression matches.
The forbiddenNameValidator factory returns the configured validator function.
That function takes an Angular control object and returns either
null if the control value is valid or a validation error object.
The validation error object typically has a property whose name is the validation key, 'forbiddenName',
and whose value is an arbitrary dictionary of values that you could insert into an error message ({name}).
Angular forms recognizes the directive's role in the validation process because the directive registers itself
with the NG_VALIDATORS provider, a provider with an extensible collection of validation directives.
If you are familiar with Angular validations, you may have noticed
that the custom validation directive is instantiated with useExisting
rather than useClass. The registered validator must be this instance of
the ForbiddenValidatorDirective—the instance in the form with
its forbiddenName property bound to “bob". If you were to replace
useExisting with useClass, then you’d be registering a new class instance, one that
doesn’t have a forbiddenName.
To see this in action, run the example and then type “bob” in the name of Hero Form 2.
Notice that you get a validation error. Now change from useExisting to useClass and try again.
This time, when you type “bob”, there's no "bob" error message.
For more information on attaching behavior to elements,
see Attribute Directives.
测试时的注意事项
Testing Considerations
我们可以为响应式表单的验证器和控制器逻辑编写孤立单元测试。
You can write isolated unit tests of validation and control logic in Reactive Forms.
孤立单元测试直接检测组件类,与组件和它的模板的交互、DOM、其他以来和Angular本省都无关。
Isolated unit tests probe the component class directly, independent of its
interactions with its template, the DOM, other dependencies, or Angular itself.
That's not possible with template-driven forms.
The template-driven approach relies on Angular to produce the control model and
to derive validation rules from the HTML validation attributes.
You must use the Angular TestBed to create component test instances,
write asynchronous tests, and interact with the DOM.