Angular 4.x Template-Driven Forms

Angular 4.x 中有兩種表單:html

  • Template-Driven Forms - 模板驅動式表單 (相似於 AngularJS 1.x 中的表單 )react

  • Reactive Forms - 響應式表單typescript

本文主要介紹 Template-Driven Forms (模板驅動式表單) ,將涉及 ngFormngModelngModelGroup、表單提交事件、表單驗證和異常信息輸出等內容。json

Contents

  • ngModule and template-driven formsbootstrap

  • Binding ngForm and ngModel瀏覽器

  • ngModel,[ngModel] and [(ngModel)]安全

  • ngModels and ngModelGroupangular2

  • Template-driven submitapp

  • Template-driven error validationide

Form base and interface

Form base

<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 輸入框的值

  • 爲全部輸入框添加表單驗證功能

  • 顯示驗證異常信息

  • 表單驗證失敗時,不容許進行表單提交

  • 表單提交功能

User interface

// signup.interface.ts
export interface User {
  name: string;
  account: {
    email: string;
    confirm: string;
  }
}

ngModule and template-driven forms

在咱們繼續深刻介紹 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。

Template-driven approach

使用模板驅動的表單,咱們基本上能夠將組件類留空,直到咱們須要讀取/寫入值 (例如提交和設置初始值)。咱們將基於上面的定義的基礎表單,建立 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 輸入框的值。

Binding ngForm and ngModel

咱們從 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 中怎麼實現數據綁定。

ngModel,[ngModel] and [(ngModel)]

在 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)] 是簡寫的語法糖。

ngModels and ngModelGroup

咱們已經介紹了 ngFormngModel 的基礎用法,如今咱們來完善剩下的內容。SignupFormComponent 組件類的用戶模型中,包含了一個嵌套屬性 account ,account 對象中包含 emailconfirm 屬性,分爲表示郵件地址和重複確認的郵件地址。針對這種場景,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 }}

此時咱們已經完成了表單數據綁定,接下來咱們來爲表單增長提交邏輯。

Template-driven submit

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 引用對象中獲取 valuevalid 屬性的值。其中 value 的值,就是 f.value 的值。表單的數據綁定方式和提交邏輯已經介紹完了,是該介紹表單實際應用中,一個重要的環節 — 表單驗證。

Template-driven error validation

在爲表單項添加驗證規則前,咱們先來更新一下 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 的值是 nullundefined 時,表示驗證成功。

友情提示:?.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 表單組添加了顯示驗證異常信息的功能。但有沒有其它更好的方式呢?有沒有辦法去掉 userEmailuserConfirm 引用對象呢?答案是確定的,具體示例以下:

<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 有什麼做用?

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

參考資源

相關文章
相關標籤/搜索