Angular 4.x 中有两种表单:html
Template-Driven Forms - 模板驱动式表单 (相似于 AngularJS 1.x 中的表单 )react
Reactive Forms - 响应式表单typescript
本文主要介绍 Template-Driven Forms (模板驱动式表单) ,将涉及 ngForm
、ngModel
、ngModelGroup
、表单提交事件、表单验证和异常信息输出等内容。json
ngModule and template-driven formsbootstrap
Binding ngForm and ngModel浏览器
ngModel,[ngModel] and [(ngModel)]安全
ngModels and ngModelGroupangular2
Template-driven submitapp
Template-driven error validationide
<form novalidate> <label> <span>Full name</span> <input type="text" name="name" placeholder="Your full name"> </label> <div> <label> <span>Email address</span> <input type="email" name="email" placeholder="Your email address"> </label> <label> <span>Confirm address</span> <input type="email" name="confirm" placeholder="Confirm your email address"> </label> </div> <button type="submit">Sign up</button> </form>
接下来咱们要实现的功能以下:
绑定 name、email、confirm 输入框的值
为全部输入框添加表单验证功能
显示验证异常信息
表单验证失败时,不容许进行表单提交
表单提交功能
// signup.interface.ts export interface User { name: string; account: { email: string; confirm: string; } }
在咱们继续深刻介绍 template-driven 表单前,咱们必须在 @NgModule
中导入 @angular/forms
库中的 FormModule
:
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ ..., FormsModule ], declarations: [...], bootstrap: [...] }) export class AppModule {}
友情提示:若使用 template-driven 表单,则导入 FormsModule;若使用 reactive forms,则导入 ReactiveFormsModule。
使用模板驱动的表单,咱们基本上能够将组件类留空,直到咱们须要读取/写入值 (例如提交和设置初始值)。咱们将基于上面的定义的基础表单,建立 SignupFormComponent
:
signup-form.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'signup-form', template: ` <form novalidate>...</form> ` }) export class SignupFormComponent { constructor() {} }
这是一个很基础的组件,接下来咱们导入以前定义的 User
接口,具体以下:
import { User } from './signup.interface'; @Component({...}) export class SignupFormComponent { public user: User = { name: '', account: { email: '', confirm: '' } }; }
初始化 SignupFormComponent 组件类中的用户模型后,咱们开始实现第一个功能点:即绑定 name、email、confirm 输入框的值。
咱们从 ngForm 开始,更新后的模板以下:
<form novalidate #f="ngForm"> <label> <span>Full name</span> <input type="text" placeholder="Your full name"> </label> </form>
上面代码中,咱们把 ngForm
的值赋值给 #f
变量,经过该变量咱们能够方便的获取表单的值。
友情提示:#f 变量的值,是 ngForm 指令的导出对象。
@Directive({ selector: 'form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]', providers: [formDirectiveProvider], host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'}, outputs: ['ngSubmit'], exportAs: 'ngForm' }) export class NgForm extends ControlContainer implements Form {}
在模板中,咱们能够经过如下方式查看表单的值:
{{ f.value | json }} // {}
上面示例 f.value
输出 {},由于此时咱们表单中还未绑定任何值。在 Angular 1.x 中咱们能够使用 ng-model
指令进行表单数据的双向绑定,接下来咱们来看一下 Angular 4.x 中怎么实现数据绑定。
在 Angular 4.x 中 ngModel
有三种不一样的语法:
ngModel - 直接使用 ngModel 指令,没有使用绑定或关联任何值。此时,ngModel 将自动关联表单控件的 name 属性,并使用该值做为 ngForm 对象的属性名。
<form novalidate #f="ngForm"> ... <input type="text" placeholder="Your full name" name="name" ngModel> ... </form>
友情提示:上面示例中,若是 input 输入框若未设置 name 属性,应用将会抛出异常。ngModel 指令基于输入框的 name 属性,进行绑定。
运行以上代码,f.value 的输入值以下:
{{ f.value | json }} // { name: '' }
很是好,咱们已经绑定了 name 输入框的值。但咱们应该怎么为输入框设置初始值?
[ngModel] = one-way binding syntax (单向绑定语法)
为了设置输入框初始值,咱们先要更新一下 SignupFormComponent 组件类的用户模型:
... user: User = { name: 'Semlinker', account: { email: '', confirm: '' } }; ...
更新完用户模型,咱们须要同步更新组件模板,具体以下:
<form #f="ngForm"> ... <input type="text" placeholder="Your full name" name="name" [ngModel]="user.name"> ... </form>
代码从新运行后,f.value 的输出以下:
{{ f.value | json }} // { name: 'Semlinker' }
从上面示例能够看出,使用 [ngModel]
容许咱们经过 this.user.name
设置 name 输入框的初始值,并且该值会自动绑定到 f.value
对象上。
友情提示:[ngModel] 是单向绑定,当表单中 name 输入框的值改变时,不会同步更新 this.user.name
若是想在 name 输入框值变化时,自动同步更新 this.user.name
的值,咱们须要使用双向绑定。
[(ngModel)] = two-way binding syntax (双向绑定),具体示例以下:
<form #f="ngForm"> ... <input type="text" placeholder="Your full name" name="name" [(ngModel)]="user.name"> ... </form>
上面示例成功运行后,咱们能够在模板中新增如下代码,而后观察 user
模型的值:
{{ user | json }} // { name: 'Semlinker' }
须要注意的是,如下两种方式是等价的:
<input [(ngModel)]="user.name"> <input [ngModel]="user.name" (ngModelChange)="user.name = $event">
其中 [(ngModel)]
是简写的语法糖。
咱们已经介绍了 ngForm
和 ngModel
的基础用法,如今咱们来完善剩下的内容。SignupFormComponent 组件类的用户模型中,包含了一个嵌套属性 account
,account 对象中包含 email
和 confirm
属性,分为表示邮件地址和重复确认的邮件地址。针对这种场景,Angular 4.x 为咱们提供了 ngModelGroup
指令,具体示例以下:
<form novalidate #f="ngForm"> <label> <span>Full name</span> <input type="text" placeholder="Your full name" name="name" ngModel> </label> <div ngModelGroup="account"> <label> <span>Email address</span> <input type="email" placeholder="Your email address" name="email" ngModel> </label> <label> <span>Confirm address</span> <input type="email" placeholder="Confirm your email address" name="confirm" ngModel> </label> </div> <button type="submit">Sign up</button> </form>
使用 ngModelGroup
指令后,咱们的 DOM 结构将更加合理:
ngForm -> '#f' ngModel -> 'name' ngModelGroup -> 'account' -> ngModel -> 'email' -> ngModel -> 'confirm'
以上代码成功运行后,浏览器中页面显示的结果:
// { name: 'Semlinker', account: { email: '', confirm: '' } } {{ f.value | json }}
此时咱们已经完成了表单数据绑定,接下来咱们来为表单增长提交逻辑。
Angular 表单中提供了 ngSubmit
输出属性,用于监听表单的提交事件:
<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm"> ... </form>
当用户提交表单时,咱们将会把 f
做为参数,调用 ngSubmit
关联的 onSubmit()
方法。onSubmit() 方法的具体实现以下:
export class SignupFormComponent { user: User = {...}; onSubmit({ value, valid }: { value: User, valid: boolean }) { console.log(value, valid); } }
上面代码中,咱们使用 Object destructuring
(对象解构) 的方式,从#f
引用对象中获取 value
和 valid
属性的值。其中 value
的值,就是 f.value
的值。表单的数据绑定方式和提交逻辑已经介绍完了,是该介绍表单实际应用中,一个重要的环节 — 表单验证。
在为表单项添加验证规则前,咱们先来更新一下 SignupFormComponent 组件中的 Sign up
按钮,确保在表单验证不经过时,不容许用户执行表单提交操做。更新后的代码以下:
<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm"> ... <button type="submit" [disabled]="f.invalid">Sign up</button> </form>
以上代码咱们经过 f.invalid
获取表单当前的验证状态 (验证不经过时该值为true),来控制按钮的 disabled
属性。
接下来开始进入正题,为表单添加验证规则:
<form novalidate #f="ngForm"> <label> ... <input ... ngModel required> </label> <div ngModelGroup="account"> <label> ... <input ... name="email" ngModel required> </label> <label> ... <input ... name="confirm" ngModel required> </label> </div> <button type="submit">Sign up</button> </form>
上面代码中,咱们为每一个 input 表单控件,添加了 required
(必填项) 的验证规则。一切都那么简单,剩下的问题就是如何获取验证失败的异常消息。
皇上,您还记得当年大明湖畔的夏雨荷吗? — No,No,No !我只记得安谷拉 (angular) 湖畔的美女 (f)。
#f
引用对象中有一个 controls
属性,经过该属性,咱们就能够获取表单控件的验证信息,下面示例演示了如何获取 name 表单控件验证的异常信息:
<form novalidate #f="ngForm"> {{ f.controls.name?.errors | json }} </form>
f.controls.name?.errors
的值是 null
或 undefined
时,表示验证成功。
友情提示:?.prop 称为安全导航操做符,用于告诉 Angular prop 的值可能不存在。
接下来为咱们的 name 表单控件,添加显示异常信息的代码:
<div *ngIf="f.controls.name?.required" class="error"> Name is required </div>
虽然咱们已经能够获取某个表单项的验证信息,但有没有以为使用 f.controls.name?.errors
这种方式,太麻烦了。那么有没有更简单的方式呢?个人答案是 - Yes !废话很少说,立刻看示例:
<label> ... <input ... #userName="ngModel" required> </label> <div *ngIf="userName.errors?.required" class="error"> Name is required </div>
(备注:此处必定要使用 #userName="ngModel"
)
以上代码成功运行后,咱们在浏览器中看到了异常信息,为了不一开始就显示异常信息,咱们能够更新一下 *ngIf
表达式的验证逻辑:
<div *ngIf="userName.errors?.required && userName.touched" class="error"> Name is required </div>
除了使用 required
验证规则以外,咱们还能够使用 minlength
(最小长度)、maxlength
(最大长度) 等验证规则,下面咱们继续来完善 SignupFormComponent 组件的功能,即为其它的表单控件添加显示异常信息的功能:
<!-- name --> <div *ngIf="userName.errors?.required && userName.touched" class="error"> Name is required </div> <div *ngIf="userName.errors?.minlength && userName.touched" class="error"> Minimum of 2 characters </div> <!-- account: { email, confirm } --> <div *ngIf="userEmail.errors?.required && userEmail.touched" class="error"> Email is required </div> <div *ngIf="userConfirm.errors?.required && userConfirm.touched" class="error"> Confirming email is required </div>
咱们经过使用模板变量的方式,为 account 表单组添加了显示验证异常信息的功能。但有没有其它更好的方式呢?有没有办法去掉 userEmail
和 userConfirm
引用对象呢?答案是确定的,具体示例以下:
<div ngModelGroup="account" #userAccount="ngModelGroup"> <label> <span>Email address</span> <input type="email" placeholder="Your email address" name="email" ngModel required> </label> <label> <span>Confirm address</span> <input type="email" placeholder="Confirm your email address" name="confirm" ngModel required> </label> <div *ngIf="userAccount.invalid && userAccount.touched" class="error"> Both emails are required </div> </div>
(备注:记得移除模板上的 #userEmail
和 #userConfirm
引用哈)
touched
外,还包含其它几种状态?表单控件有如下 6 种状态,咱们能够经过 #userName="ngModel"
方式获取对应的状态值。具体状态以下:
valid - 表单控件有效
invalid - 表单控件无效
pristine - 表单控件值未改变
dirty - 表单控件值已改变
touched - 表单控件已被访问过
untouched - 表单控件未被访问过
#userName
和 #userName="ngModel"
这两种方式有什么区别?#userName
- 指向 input 表单控件
#userName="ngModel"
- 指向 NgModel 实例
ngModel 指令
// angular2/packages/forms/src/directives/ng_model.ts 片断 @Directive({ selector: '[ngModel]:not([formControlName]):not([formControl])', providers: [formControlBinding], exportAs: 'ngModel' // // 导出指令实例,使得能够在模板中调用 }) export class NgModel extends NgControl implements OnChanges, OnDestroy { }
NgControl 抽象类
// angular2/packages/forms/src/directives/ng_control.ts 片断 export abstract class NgControl extends AbstractControlDirective { /** @internal */ _parent: ControlContainer = null; name: string = null; valueAccessor: ControlValueAccessor = null; ... abstract viewToModelUpdate(newValue: any): void; }
AbstractControlDirective 抽象类
// angular2/packages/forms/src/directives/abstract_control_directive.ts 片断 export abstract class AbstractControlDirective { get valid(): boolean { return this.control ? this.control.valid : null; } get invalid(): boolean { return this.control ? this.control.invalid : null; } get errors(): ValidationErrors | null { return this.control ? this.control.errors : null; } get pristine(): boolean { return this.control ? this.control.pristine : null; } get dirty(): boolean { return this.control ? this.control.dirty : null; } get touched(): boolean { return this.control ? this.control.touched : null; } get untouched(): boolean { return this.control ? this.control.untouched : null; } get valueChanges(): Observable<any> { return this.control ? this.control.valueChanges : null; } hasError(errorCode: string, path: string[] = null): boolean { return this.control ? this.control.hasError(errorCode, path) : false; } getError(errorCode: string, path: string[] = null): any { return this.control ? this.control.getError(errorCode, path) : null; } }
ngModelGroup 指令是 Angular 提供的另外一特殊指令,能够对表单输入内容进行分组,方便咱们在语义上区分不一样性质的输入。例如联系人的信息包括姓名及住址,如今需对姓名和住址进行精细化信息收集,姓名可精细化成姓和名字,地址可精细化成城市、区、街等。此时就能够将姓名及住址进行分组收集,具体以下:
<form #concatForm = "ngForm"> <fieldset ngModelGroup="nameGroup" #nameGroup="ngModelGroup"> <label>姓:</label> <input type="text" name="firstname" [(ngModel)]="curContact.firstname" required> <label>名字:</label> <input type="text" name="lastname" [(ngModel)]="curContact.lastname" required> </fieldset> <fieldset ngModelGroup="addressGroup" #addressGroup ="ngModelGroup"> <label>街:</label> <input type="text" name="street" [(ngModel)]="curContact.street" required> <label>区:</label> <input type="text" name="zip" [(ngModel)]="curContact.zip" required> <label>城市:</label> <input type="text" name="city" [(ngModel)]="curContact.city" required> </fieldset> </form>
上述例子分别对联系人的姓名和住址进行分组, ngModelGroup 将姓和名字的表单内容进行包裹组成姓名分组,将城市、区和街道的表单内容进行包裹组成住址分组。此时concatForm.value值为:
{ nameGroup: { firstname: '', lastname: '', }, addressGroup: { street: '', zip: '', city: '' } }