![最终产品图片](https://cms-assets.tutsplus.com/uploads/users/1795/posts/29787/final_image/CreatingFormsinAngular-TemplateDrivenForms-MainImage.jpg)
这是“ Angular 4中的表单简介”系列的第二部分。在第一部分中,我们使用模板驱动的方法创建了一个表单。 我们使用了ngModel
, ngModelGroup
和ngForm
等指令来ngModel
表单元素。 在本教程中,我们将采用另一种方法来构建表单-反应性方法。
反应形式
与模板驱动的表单相比,反应性表单采用不同的方法。 在这里,我们在组件类中创建并初始化表单控件对象 。 它们是保存表单状态的中间对象。 然后,我们将它们绑定到模板中的表单控件元素 。
表单控件对象侦听输入控制值的任何更改,并且它们会立即反映在对象的状态中。 由于组件可以直接访问数据模型结构,因此所有更改都可以在数据模型,表单控制对象和输入控制值之间同步。
实际上,如果我们正在构建用于更新用户配置文件的表单,则数据模型是从服务器检索的用户对象。 按照约定,这通常存储在组件的用户属性( this.user
)中。 表单控件对象或表单模型将绑定到模板的实际表单控件元素。
这两个模型即使不相同,也应具有相似的结构。 但是,输入值不应直接流入数据模型。 该图像描述了用户从模板输入的信息如何进入表单模型。
让我们开始吧。
先决条件
您无需遵循本系列的第一部分,而使第二部分有意义。 但是,如果您不熟悉Angular中的表单,我强烈建议您采用模板驱动策略。 我的GitHub存储库中提供了该项目的代码。 确保您位于正确的分支上,然后下载zip或克隆存储库以查看正在使用的表单。
如果您想从头开始,请确保已安装Angular CLI。 使用ng
命令生成一个新项目。
$ ng new SignupFormProject
接下来,为SignupForm
生成一个新组件或手动创建一个组件。
ng generate component SignupForm
用以下内容替换app.component.html的内容:
<app-signup-form> </app-signup-form>
这是src /目录的目录结构。 我删除了一些不必要的文件,以使事情变得简单。
.
├── app
│ ├── app.component.css
│ ├── app.component.html
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── signup-form
│ │ ├── signup-form.component.css
│ │ ├── signup-form.component.html
│ │ └── signup-form.component.ts
│ └── User.ts
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
├── tsconfig.app.json
└── typings.d.ts
如您所见, SignupForm
组件的目录已自动创建。 那就是我们大多数代码的去向。 我还创建了一个新的User.ts
来存储我们的User模型。
HTML模板
在深入研究实际的组件模板之前,我们需要对要构建的内容有一个抽象的想法。 这就是我脑海中的表单结构。 注册表单将具有几个输入字段,一个select元素和一个checkbox元素。
这是我们将用于注册页面HTML模板。
HTML模板
<div class="row custom-row">
<div class= "col-sm-5 custom-container jumbotron">
<form class="form-horizontal">
<fieldset>
<legend>SignUp</legend>
<!--- Email Block --->
<div class="form-group">
<label for="inputEmail">Email</label>
<input type="text"
id="inputEmail"
placeholder="Email">
</div>
<!--- Password Block --->
<div class="form-group">
<label for="inputPassword">Password</label>
<input type="password"
id="inputPassword"
placeholder="Password">
</div>
<div class="form-group">
<label for="confirmPassword" >Confirm Password</label>
<input type="password"
id="confirmPassword"
placeholder="Password">
</div>
<!--- Select gender Block --->
<div class="form-group">
<label for="select">Gender</label>
<select id="select">
<option>Male</option>
<option>Female</option>
<option>Other</option>
</select>
</div>
<!--- Terms and conditions Block --->
<div class="form-group checkbox">
<label>
<input type="checkbox"> Confirm that you've read the Terms and
Conditions
</label>
</div>
<!--- Buttons Block --->
<div class="form-group">
<button type="reset" class="btn btn-default">Cancel</button>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</fieldset>
</form>
</div>
</div>
HTML模板中使用CSS类是用于使外观漂亮的Bootstrap库的一部分。 由于这不是设计教程,因此除非有必要,否则我不会谈论表单CSS方面。
基本表格设置
要创建Reactive表单,您需要从@angular/forms
导入ReactiveFormsModule
并将其添加到app.module.ts中的imports数组中。
app / app.module.ts
// Import ReactiveFormsModule
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
.
.
//Add the module to the imports Array
imports: [
BrowserModule,
ReactiveFormsModule
.
.
})
export class AppModule { }
接下来,为注册表单创建一个用户模型。 我们可以使用类或接口来创建模型。 对于本教程,我将导出具有以下属性的类。
app / User.ts
export class User {
id: number;
email: string;
//Both the passwords are in a single object
password: {
pwd: string;
confirmPwd: string;
};
gender: string;
terms: boolean;
constructor(values: Object = {}) {
//Constructor initialization
Object.assign(this, values);
}
}
现在,在SignupForm
组件中创建User模型的实例。
app / signup-form / signup-form.component.ts
import { Component, OnInit } from '@angular/core';
// Import the User model
import { User } from './../User';
@Component({
selector: 'app-signup-form',
templateUrl: './signup-form.component.html',
styleUrls: ['./signup-form.component.css']
})
export class SignupFormComponent implements OnInit {
//Gender list for the select control element
private genderList: string[];
//Property for the user
private user:User;
ngOnInit() {
this.genderList = ['Male', 'Female', 'Others'];
}
对于signup-form.component.html文件,我将使用上面讨论的相同HTML模板,但会有一些细微变化。 注册表单具有一个带有选项列表的选择字段。 尽管这ngFor
,但我们将通过使用ngFor
指令遍历列表来以Angular方式进行ngFor
。
app / signup-form / signup-form.component.html
<div class="row custom-row">
<div class= "col-sm-5 custom-container jumbotron">
<form class="form-horizontal">
<fieldset>
<legend>SignUp</legend>
.
.
<!--- Gender Block -->
<div class="form-group">
<label for="select">Gender</label>
<select id="select">
<option *ngFor = "let g of genderList"
[value] = "g"> {{g}}
</option>
</select>
</div>
.
.
</fieldset>
</form>
</div>
</div>
注意:您可能会收到一条错误消息,指出 没有为ControlContainer提供程序 。 当组件具有不带formGroup指令的<form>标记时,将出现错误。 一旦在本教程的后面添加了FormGroup指令,该错误将消失。
我们手边有一个组件,一个模型和一个表单模板。 现在怎么办? 现在是时候让我们变脏了,熟悉创建反应式表单所需的API。 这包括FormControl
和FormGroup
。
使用FormControl跟踪状态
使用反应式表单策略构建表单时,您不会遇到ngModel和ngForm指令。 相反,我们使用基础的FormControl和FormGroup API。
FormControl是用于创建FormControl实例的指令,可用于跟踪特定表单元素的状态及其验证状态。 这是FormControl的工作方式:
/* Import FormControl first */
import { FormControl } from '@angular/forms';
/* Example of creating a new FormControl instance */
export class SignupFormComponent {
email = new FormControl();
}
email
现在是一个FormControl实例,您可以将其绑定到模板中的输入控件元素,如下所示:
<h2>Signup</h2>
<label class="control-label">Email:
<input class="form-control" [formControl]="email">
</label>
现在,模板表单元素已绑定到组件中的FormControl实例。 这意味着对输入控制值的任何更改都会反映在另一端。
FormControl构造函数接受三个参数-初始值,同步验证器数组和异步验证器数组-正如您可能已经猜到的,它们都是可选的。 我们将在这里讨论前两个论点。
import { Validators } from '@angular/forms';
.
.
.
/* FormControl with initial value and a validator */
email = new FormControl('bob@example.com', Validators.required);
Angular具有一组有限的内置验证器。 流行的验证器方法包括Validators.required
, Validators.minLength
, Validators.maxlength
和Validators.pattern
。 但是,要使用它们,必须首先导入Validator API。
对于我们的注册表单,我们有多个输入控制字段(用于电子邮件和密码),一个选择器字段和一个复选框字段。 而不是创建单个FormControl
对象,将所有这些FormControl
归为一个实体更有意义吗? 这是有益的,因为我们现在可以在一处跟踪所有子FormControl对象的值和有效性。 那就是FormGroup
目的。 因此,我们将使用多个子FormControl注册一个父FormGroup。
使用FormGroup将多个FormControl分组
要添加FormGroup,请首先将其导入。 接下来,将signupForm声明为类属性,并按如下所示对其进行初始化:
app / signup-form / signup-form.component.ts
//Import the API for building a form
import { FormControl, FormGroup, Validators } from '@angular/forms';
export class SignupFormComponent implements OnInit {
genderList: String[];
signupForm: FormGroup;
.
.
ngOnInit() {
this.genderList = ['Male', 'Female', 'Others'];
this.signupForm = new FormGroup ({
email: new FormControl('',Validators.required),
pwd: new FormControl(),
confirmPwd: new FormControl(),
gender: new FormControl(),
terms: new FormControl()
})
}
}
如下所示将FormGroup模型绑定到DOM:
app / signup-form / signup-form.component.html
<form class="form-horizontal" [formGroup]="signupForm" >
<fieldset>
<legend>SignUp</legend>
<!--- Email Block -->
<div class="form-group">
<label for="inputEmail">Email</label>
<input type="text" formControlName = "email"
id="inputEmail"
placeholder="Email">
.
.
</fieldset>
</form>
[formGroup] = "signupForm"
告诉Angular您想将此表单与组件类中声明的FormGroup
关联。 当Angular看到formControlName="email"
,它将检查父FormGroup中具有键值email
的FormControl实例。
同样,通过添加formControlName="value"
属性来更新其他表单元素,就像我们在这里所做的那样。
若要查看是否一切正常,请在form标记后添加以下内容:
app / signup-form / signup-form.component.html
<!--- Log the FormGroup values to see if the binding is working -->
<p>Form value {{ signupForm.value | json }} </p>
<p> Form status {{ signupForm.status | json}} </p>
通过JsonPipe
传递SignupForm
属性,以在浏览器中将模型呈现为JSON。 这对于调试和记录很有帮助。 您应该看到这样的JSON输出。
这里有两件事要注意:
- JSON与我们之前创建的用户模型的结构不完全匹配。
- signupForm.status显示表单的状态为INVALID。 这清楚地表明,电子邮件控制字段上的
Validators.required
可以正常工作。
表单模型和数据模型的结构应匹配。
// Form model
{
"email": "",
"pwd": "",
"confirmPwd": "",
"gender": "",
"terms": false
}
//User model
{
"email": "",
"password": {
"pwd": "",
"confirmPwd": "",
},
"gender": "",
"terms": false
}
为了获得数据模型的层次结构,我们应该使用嵌套的FormGroup。 此外,在单个FormGroup下具有相关的表单元素总是一个好主意。
嵌套表格组
为密码创建一个新的FormGroup。
app / signup-form / signup-form.component.ts
this.signupForm = new FormGroup ({
email: new FormControl('',Validators.required),
password: new FormGroup({
pwd: new FormControl(),
confirmPwd: new FormControl()
}),
gender: new FormControl(),
terms: new FormControl()
})
现在,要将新表单模型与DOM绑定,请进行以下更改:
app / signup-form / signup-form.component.html
<!--- Password Block -->
<div formGroupName = "password">
<div class="form-group">
<label for="inputPassword">Password</label>
<input type="password" formControlName = "pwd"
id="inputPassword"
placeholder="Password">
</div>
<div class="form-group">
<label for="confirmPassword" >Confirm Password</label>
<input type="password" formControlName = "confirmPwd"
id="confirmPassword"
placeholder="Password">
</div>
</div>
formGroupName = "password"
对嵌套的FormGroup执行绑定。 现在,表单模型的结构符合我们的要求。
Form value: {
"email": "", "
password": { "pwd": null, "confirmPwd": null },
"gender": null,
"terms": null
}
Form status "INVALID"
接下来,我们需要验证表单控件。
验证表格
我们已经对电子邮件输入控件进行了简单的验证。 但是,这还不够。 这是我们验证要求的完整列表。
- 所有表单控制元素都是必需的 。
- 禁用提交按钮,直到表单状态为VALID。
- 电子邮件字段应严格包含电子邮件ID。
- 密码字段的最小长度为8。
第一个很简单。 将Validator.required
添加到表单模型中的所有FormControls中。
app / signup-form / signup-form.component.ts
this.signupForm = new FormGroup ({
email: new FormControl('',Validators.required),
password: new FormGroup({
pwd: new FormControl('', Validators.required),
confirmPwd: new FormControl('', Validators.required)
}),
gender: new FormControl('', Validators.required),
//requiredTrue so that the terms field isvalid only if checked
terms: new FormControl('', Validators.requiredTrue)
})
接下来,在表单无效时禁用按钮。
app / signup-form / signup-form.component.html
<!--- Buttons Block -->
<div class="form-group">
<button type="reset" class="btn btn-default">Cancel</button>
<button type="submit" [disabled] = "!signupForm.valid" class="btn btn-primary">Submit</button>
</div>
要对电子邮件添加约束,您可以使用默认的Validators.email
或创建一个自定义的Validators.pattern()
来指定正则表达式,例如以下表达式:
email: new FormControl('',
[Validators.required,
Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$')])
将minLength
验证器用于密码字段。
password: new FormGroup({
pwd: new FormControl('', [Validators.required, Validators.minLength(8)]),
confirmPwd: new FormControl('', [Validators.required, Validators.minLength(8)])
}),
验证就可以了。 但是,表单模型逻辑显得混乱且重复。 让我们先清理一下。
使用FormBuilder重构代码
Angular为您提供了一个语法糖,用于创建称为FormBuilder的FormGroup和FormControl的新实例。 除了我们在这里介绍的内容外,FormBuilder API并没有做任何特别的事情。
它简化了我们的代码,并使构建表单的过程更加轻松。 要创建FormBuilder,必须将其导入signup-form.component.ts并将FormBuilder注入构造函数。
app / signup-form / signup-form.component.ts
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
.
.
export class SignupFormComponent implements OnInit {
signupForm: FormGroup; // Declare the signupForm
//Inject the formbuilder into the constructor
constructor(private fb:FormBuilder) {}
ngOnInit() {
...
}
}
而不是创建一个新的FormGroup()
,我们使用this.fb.group
来构建一个表单。 除语法外,其他所有内容均保持不变。
app / signup-form / signup-form.component.ts
ngOnInit() {
...
this.signupForm = this.fb.group({
email: ['',[Validators.required,
Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$')]],
password: this.fb.group({
pwd: ['', [Validators.required,
Validators.minLength(8)]],
confirmPwd: ['', [Validators.required,
Validators.minLength(8)]]
}),
gender: ['', Validators.required],
terms: ['', Validators.requiredTrue]
})
}
显示验证错误
为了显示错误,我将在div元素上使用条件指令ngIf
。 让我们从电子邮件的输入控制字段开始:
<!-- Email error block -->
<div *ngIf="signupForm.controls.email.invalid && signupForm.controls.email.touched"
Email is invalid
</div>
这里有几个问题。
-
invalid
和pristine
来自何处? -
signupForm.controls.email.invalid
太长太深。 - 该错误没有明确说明为什么无效。
为了回答第一个问题,每个FormControl都具有某些属性,例如invalid
, valid
, pristine
, dirty
, touched
和untouched
。 我们可以使用它们来确定是否应该显示错误消息或警告。 下图详细描述了每个属性。
因此,仅当电子邮件无效时,才会显示带有*ngIf
的div元素。 但是,即使在他们有机会编辑表单之前,也会给用户带来关于输入字段为空白的错误消息。
为了避免这种情况,我们添加了第二个条件。 仅在访问控件后才会显示错误。
为了摆脱方法名称的长链( signupForm.controls.email.invalid
),我将添加几个速记方法。 这使它们更易于访问和简短。
app / signup-form / signup-form.component.ts
export class SignupFormComponent implements OnInit {
...
get email() { return this.signupForm.get('email'); }
get password() { return this.signupForm.get('password'); }
get gender() { return this.signupForm.get('gender'); }
get terms() { return this.signupForm.get('terms'); }
}
为了使错误更明确,我在下面添加了嵌套的ngIf条件:
app / signup-form / signup-form.component.html
<!-- Email error block -->
<div *ngIf="email.invalid && email.touched"
class="col-sm-3 text-danger">
<div *ngIf = "email.errors?.required">
Email field can't be blank
</div>
<div *ngIf = "email.errors?.pattern">
The email id doesn't seem right
</div>
</div>
我们使用email.errors
来检查所有可能的验证错误,然后以自定义消息的形式将其显示给用户。 现在,对其他表单元素执行相同的步骤。 这是我对密码和术语输入控件的验证进行编码的方式。
app / signup-form / signup-form.component.html
<!-- Password error block -->
<div *ngIf="(password.invalid && password.touched)"
class="col-sm-3 text-danger">
Password needs to be more than 8 characters
</div>
.
.
.
<!--- Terms error block -->
<div *ngIf="(terms.invalid && terms.touched)"
class="col-sm-3 text-danger">
Please accept the Terms and conditions first.
</div>
</div>
使用ngSubmit提交表单
我们几乎完成了表格。 它缺少我们即将实现的提交功能。
<form class="form-horizontal"
[formGroup]="signupForm"
(ngSubmit)="onFormSubmit()" >
在提交表单时,表单模型值应流入组件的用户属性。
app / signup-form / signup-form.component.ts
public onFormSubmit() {
if(this.signupForm.valid) {
this.user = this.signupForm.value;
console.log(this.user);
/* Any API call logic via services goes here */
}
}
包起来
如果您从一开始就一直关注本教程系列,那么我们将对Angular中的两种流行的表单构建技术有实际经验。 模板驱动和模型驱动技术是实现同一目标的两种方法。 就我个人而言,出于以下原因,我更喜欢使用反应形式:
- 所有表单验证逻辑都将位于组件类内部的单个位置。 与ngModel指令分散在整个模板中的模板方法相比,这种方法的生产率更高。
- 与模板驱动的表单不同,模型驱动的表单更易于测试。 您不必借助端到端测试库来测试您的表单。
- 验证逻辑将进入组件类而不是模板。
- 对于具有大量表单元素的表单,此方法具有一个称为FormBuilder的东西,可以简化FormControl对象的创建。
我们错过了一件事,那就是写一个密码不匹配的验证器。 在本系列的最后一部分,我们将介绍在Angular中创建自定义验证器函数所需的所有知识。 敬请期待。
同时,有许多框架和库让您不忙,在Envato Market上有很多项目可供阅读,研究和使用。
翻译自: https://code.tutsplus.com/tutorials/introduction-to-forms-in-angular-4-reactive-forms--cms-29787