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: '' } }