国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

詳談Angular 2+ 的表單(一)之模板驅(qū)動型表單

2019-11-19 16:43:55
字體:
供稿:網(wǎng)友

摘要

在企業(yè)應(yīng)用開發(fā)時,表單是一個躲不過去的事情,和面向消費者的應(yīng)用不同,企業(yè)領(lǐng)域的開發(fā)中,表單的使用量是驚人的。這些表單的處理其實是一個挺復雜的事情,比如有的是涉及到多個 Tab 的表單,有的是向?qū)问蕉鄠€步驟的,各種復雜的驗證邏輯和時不時需要彈出的對話框等等。筆者試圖在這一系列文章中對 Angular 中的表單處理做一個相對完整的梳理。

Angular 中提供兩種類型的表單處理機制,一種叫模版驅(qū)動型(Template Driven)的表單,另一種叫模型驅(qū)動型表單( Model Driven ),這后一種也叫響應(yīng)式表單 ( Reactive Forms ),由于模版驅(qū)動中有一個 ngModel 的指令,容易和這里說的模型驅(qū)動混淆,所以在我們的文章中叫后一種說法:響應(yīng)式表單。

第一篇主要介紹模版驅(qū)動型的表單。

號外

本文評論區(qū)會抽出5位童鞋,贈送筆者的 《Angular 從零到一》紙書,機不可失,大家踴躍發(fā)言哦。

模版驅(qū)動的表單

模版驅(qū)動的表單和 AngularJS 對于表單的處理類似,把一些指令(比如 ngModel )、數(shù)據(jù)值和行為約束(比如 require 、 minlength 等等)綁定到模版中(模版就是組件元數(shù)據(jù) @Component 中定義的那個 template ),這也是模版驅(qū)動這個叫法的來源。總體來說,這種類型的表單通過綁定把很多工作交給了模版。

模版驅(qū)動的例子

還是用例子來說話,比如我們有一個用戶注冊的表單,用戶名就是 email ,還需要填的信息有:住址、密碼和重復密碼。這個應(yīng)該是比較常見的一個注冊時需要的信息了。那么我們第一步來建立領(lǐng)域模型:

// src/app/domain/index.tsexport interface User { // 新的用戶id一般由服務(wù)器自動生成,所以可以為空,用 ? 標示 id?: string;  email: string; password: string; repeat: string; address: Address;}export interface Address { province: string; // 省份 city: string; // 城市 area: string; // 區(qū)縣 addr: string; // 詳細地址}

接下來我們建立模版文件,一個最簡單的 HTML 模版,先不增加任何的綁定或事件處理:

<!-- template-driven.component.html --><form novalidate> <label> <span>電子郵件地址</span> <input  type="text"  name="email"  placeholder="請輸入您的 email 地址"> </label> <div> <label>  <span>密碼</span>  <input  type="password"  name="password"  placeholder="請輸入您的密碼"> </label> <label>  <span>確認密碼</span>  <input  type="password"  name="repeat"  placeholder="請再次輸入密碼"> </label> </div> <div > <label>  <span>省份</span>  <select name="province">  <option value="">請選擇省份</option>  </select> </label> <label>  <span>城市</span>  <select name="city">  <option value="">請選擇城市</option>  </select> </label> <label>  <span>區(qū)縣</span>  <select name="area">  <option value="">請選擇區(qū)縣</option>  </select> </label> <label>  <span>地址</span>  <input type="text" name="addr"> </label> </div> <button type="submit">注冊</button></form>

渲染之后的效果就像下面這樣:

 

簡單的Form

數(shù)據(jù)綁定

對于模版驅(qū)動型的表單處理,我們首先需要在對應(yīng)的模塊中引入 FormsModule ,這一點千萬不要忘記了。

import { NgModule } from '@angular/core';import { CommonModule } from '@angular/common';import { FormsModule } from "@angular/forms";import { TemplateDrivenComponent } from './template-driven/template-driven.component';@NgModule({ imports: [ CommonModule, FormsModule ], exports: [TemplateDrivenComponent], declarations: [TemplateDrivenComponent]})export class FormDemoModule { }進行模版驅(qū)動類型的表單處理的一個必要步驟就是建立數(shù)據(jù)的雙向綁定,那么我們需要在組件中建立一個類型為 User 的成員變量并賦初始值。// template-driven.component.ts// 省略元數(shù)據(jù)和導入的類庫信息export class TemplateDrivenComponent implements OnInit { user: User = { email: '', password: '', repeat: '', address: {  province: '',  city: '',  area: '',  addr: '' } }; // 省略其他部分}

有了這樣一個成員變量之后,我們在組件模版中就可以使用 ngModel 進行綁定了。

令人困惑的 ngModel

我們在 Angular 中可以使用三種形式的 ngModel 表達式: ngModel , [ngModel] 和 [(ngModel)] 。但無論那種形式,如果你要使用 ngModel 就必須為該控件(比如下面的 input )指定一個 name 屬性,如果你忘記添加 name 的話,多半你會看到下面這樣的錯誤:

ERROR Error: Uncaught (in promise): Error: If ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.

ngModel 和 FormControl

假如我們使用的是 ngModel ,沒有任何中括號小括號的話,這代表著我們創(chuàng)建了一個 FormControl 的實例,這個實例將會跟蹤值的變化、用戶的交互、驗證狀態(tài)以及保持視圖和領(lǐng)域?qū)ο蟮耐降裙ぷ鳌?/p>

<input type="text" name="email" placeholder="請輸入您的 email 地址" ngModel>

如果我們將這個控件放在一個 Form 表單中, ngModel 會自動將這個 FormControl 注冊為 Form 的子控件。下面的例子中我們在 <form> 中加上了 ngForm 指令,聲明這是一個 Angular 可識別的表單,而 ngModel 會將 <input> 注冊成表單的子控件,這個子控件的名字就是 email ,而且 ngModel 會基于這個子控件的值去綁定表單的的值,這也是為什么需要顯式聲明 name 的原因。

其實在我們導入 FormsModule 的時候,所有的 <form> 標簽都會默認的被認為是一個 NgForm ,因此我們并不需要顯式的在標簽中寫 ngForm 這個指令。

<!-- ngForm 并不需要顯示聲明,任何 <form> 標簽?zāi)J都是 ngForm --><form novalidate ngForm> <input type="text" name="email" placeholder="請輸入您的 email 地址" ngModel></form>

這一切現(xiàn)在都是不可見的,所以大家可能還是有些困惑,那么下面我們將其“可視化”,這需要我們引用一下表單對象,所以我們使用 #f="ngForm" 以便我們可以在模版中輸出表單的一些特性。

<!-- 使用 # 把表單對象導出到 f 這個可引用變量中 --><form novalidate #f="ngForm"> ...</form><!-- 將表單的值以 JSON 形式輸出 -->{{f.value | json}}

這時如果我們在 email 中輸入 sss ,可以看到下圖的以 JSON 形式出現(xiàn)的表單值:

 

控件的輸入值同步到了表單的值中

單向數(shù)據(jù)綁定

那么接下來,我們看看 [ngModel] 有什么用?如果我們想給控件設(shè)置一個初始值怎么辦呢,這時就需要進行一個單向綁定,方向是從組件到視圖。我們可以做的是在初始化 User 的時候,將 email 屬性設(shè)置成 wang@163.com

user: User = { email: 'wang@163.com', ... };

而且在模版中使用 [ngModel]="user.email" 進行單向綁定,這個語法其實和普通的屬性綁定是一樣的,用中括號標示這是一個要進行數(shù)據(jù)綁定的屬性,等號右邊是需要綁定的值(這里是 user.email )。那么我們就可以得到下面這樣的輸出了, email 的初始值被綁定成功!

單向數(shù)據(jù)綁定

 

雙向數(shù)據(jù)綁定

但上面的例子存在一個問題,數(shù)據(jù)的綁定是單向的,也就是說,在輸入框進行輸入的時候,我們的 user 的值不會隨之改變的。為了更好的說明,我們將 user 和 表單的值同時輸出

<div> <span>user: </span> {{user | json}}</div><div> <span>表單:</span> {{f.value | json}}</div>

此時我們將默認的電子郵件改成 wang@gmail.com 的話,表單的值是改變了,但 user 并未改變。

 

輸入的值影響了表單,但不會影響領(lǐng)域?qū)ο?/p>

如果我們希望的是在輸入時,這個輸入的值也反向的影響我們的 user 對象的值的話,那就需要用到雙向綁定了,也就是 [(ngModel)] 需要上場了。

 

表單和領(lǐng)域?qū)ο蟮闹当3至送?/p>

無論如何,這個 [()] 表達真是很奇怪的樣子,其實這個表達是一個語法糖。只要我們知道下面的兩種寫法是等價的,我們就會很清楚的理解了:用這個語法糖你就不用既寫數(shù)據(jù)綁定又寫事件綁定了。

<input [(ngModel)]="user.email"><input [ngModel]="user.email"` (ngModelChange)="user.email = $event">

ngModelGroup 是什么鬼?

如果我們仔細觀察上面的輸出的話,會發(fā)現(xiàn)一個問題: user 中是有一個嵌套對象 address 的,而表單中沒有嵌套對象的。如果要實現(xiàn)表單中的結(jié)構(gòu)和領(lǐng)域?qū)ο蟮慕Y(jié)構(gòu)一致的話,我們就得請出 ngModelGroup 了。 ngModelGroup 會創(chuàng)建并綁定一個 FormGroup 到該 DOM 元素。 FormGroup 又是什么呢?簡單來說,是一組 FormControl。

<!-- 使用 ngModelGroup 來創(chuàng)建并綁定 FormGroup --> <div ngModelGroup="address"> <label>  <span>省份</span>  <select name="province" (change)="onProvinceChange()" [(ngModel)]="user.address.province">  <option value="">請選擇省份</option>  <option [value]="province" *ngFor="let province of provinces">{{province}}</option>  </select> </label> <!-- 省略其他部分 --> </div>

這樣的話,我們再來看一下輸出,現(xiàn)在就完全一致了:

 

表單和領(lǐng)域?qū)ο蟮慕Y(jié)構(gòu)也完全一致了

數(shù)據(jù)驗證

模版驅(qū)動型的表單的驗證也是主要由模版來處理的,在看怎么使用之前,需要界定一下驗證規(guī)則:

  • 三個必填項: email , password 和 repeat
  • email 的形式需要符合電子郵件的標準
  • password 和 repeat 必須一致

當然除了這幾個規(guī)則,我們還希望在表單未驗證通過時提交按鈕是不可用的。

<form novalidate #f="ngForm"> <label> <span>電子郵件地址</span> <input  type="text"  name="email"  placeholder="請輸入您的 email 地址"  [ngModel]="user.email"  required  pattern="([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,4}"> </label> <div> <label>  <span>密碼</span>  <input  type="password"  name="password"  placeholder="請輸入您的密碼"  [(ngModel)]="user.password"  required  minlength="8"> </label> <label>  <span>確認密碼</span>  <input  type="password"  name="repeat"  placeholder="請再次輸入密碼"  [(ngModel)]="user.repeat"  required  minlength="8"> </label> </div> <!-- 省略其他部分 --> <button type="submit" [disabled]="f.invalid">注冊</button></form><div>

Angular 中有幾種內(nèi)建支持的驗證器( Validators )

  • required - 需要 FormControl 有非空值
  • minlength - 需要 FormControl 有最小長度的值
  • maxlength - 需要 FormControl 有最大長度的值
  • pattern - 需要 FormControl 的值可以匹配正則表達式

如果我們想看到結(jié)果的話,我們可以在模版中加上下面的代碼,將錯誤以 JSON 形式輸出即可。

<div> <span>email 驗證:</span> {{f.controls.email?.errors | json}}</div>

我們看到,如果不填電子郵件的話,錯誤的 JSON 是 {"required": true} ,這告訴我們目前有一個 required 的規(guī)則沒有被滿足。

 

驗證結(jié)果

當我們輸入一個字母 w 之后,就會發(fā)現(xiàn)錯誤變成了下面的樣子。這是因為我們對于 email 應(yīng)用了多個規(guī)則,當必填項滿足后,系統(tǒng)會繼續(xù)檢查其他驗證結(jié)果。

{ "pattern":  {   "requiredPattern": "^([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,4}$",   "actualValue": "w"  } }

通過幾次實驗,我們應(yīng)該可以得出結(jié)論,當驗證未通過時,驗證器返回的是一個對象, key 為驗證的規(guī)則(比如 required, minlength 等),value 為驗證結(jié)果。如果驗證通過,返回的是一個 null 。

知道這一點后,我們其實就可以做出驗證出錯的提示了,為了方便引用,我們還是導出 ngModel 到一個 email 引用,然后就可以訪問這個 FormControl 的各個屬性了:驗證的狀態(tài)( valid/invalid )、控件的狀態(tài)(是否獲得過焦點 -- touched/untouched,是否更改過內(nèi)容 -- pristine/dirty 等)

<label> <span>電子郵件地址</span> <input ... [ngModel]="user.email" #email="ngModel"></label><div *ngIf="email.errors?.required && email.touched" class="error"> email 是必填項</div><div *ngIf="email.errors?.pattern && email.touched" class="error"> email 格式不正確</div>

自定義驗證

內(nèi)建的驗證器對于兩個密碼比較的這種驗證是不夠的,那么這就需要我們自己定義一個驗證器。對于響應(yīng)式表單來說,會比較簡單一些,但對于模版驅(qū)動的表單,這需要我們實現(xiàn)一個指令來使這個驗證器更通用和更一致。因為我們希望實現(xiàn)的樣子應(yīng)該是和 required 、 minlength 等差不多的形式,比如下面這個樣子 validateEqual="repeat"

<div> <label>  <span>密碼</span>  <input  type="password"  name="password"  placeholder="請輸入您的密碼"  [(ngModel)]="user.password"  required  minlength="8"  validateEqual="repeat"> </label> <label>  <span>確認密碼</span>  <input  type="password"  name="repeat"  placeholder="請再次輸入密碼"  [(ngModel)]="user.repeat"  required  minlength="8"> </label> </div>

那么要實現(xiàn)這種形式的驗證的話,我們需要建立一個指令,而且這個指令應(yīng)該實現(xiàn) Validator 接口。一個基礎(chǔ)的框架如下:

import { Directive, forwardRef } from '@angular/core';import { NG_VALIDATORS, Validator, AbstractControl } from '@angular/forms';@Directive({ selector: '[validateEqual][ngModel]', providers: [ {   provide: NG_VALIDATORS,   useExisting: forwardRef(()=>RepeatValidatorDirective),   multi: true  } ]})export class RepeatValidatorDirective implements Validator{ constructor() { } validate(c: AbstractControl): { [key: string]: any } { return null; }}

我們還沒有開始正式的寫驗證邏輯,但上面的框架已經(jīng)出現(xiàn)了幾個有意思的點:

1.Validator 接口要求必須實現(xiàn)的一個方法是 validate(c: AbstractControl): ValidationErrors | null; 。這個也就是我們前面提到的驗證正確返回 null 否則返回一個對象,雖然沒有嚴格的約束,但其 key 一般用于表示這個驗證器的名字或者驗證的規(guī)則名字,value 一般是失敗的原因或驗證結(jié)果。

2.和組件類似,指令也有 selector 這個元數(shù)據(jù),用于選擇那個元素應(yīng)用該指令,那么我們這里除了要求 DOM 元素應(yīng)用 validateEqual 之外,還需要它是一個 ngModel 元素,這樣它才是一個 FormControl,我們在 validate 的時候才是合法的。

3.那么那個 providers 里面那些面目可憎的家伙又是干什么的呢? Angular 對于在一個 FormControl 上執(zhí)行驗證器有一個內(nèi)部機制: Angular 維護一個令牌為 NG_VALIDATORS 的 multi provider (簡單來說,Angular 為一個單一令牌注入多個值的這種形式叫 multi provider )。所有的內(nèi)建驗證器都是加到這個 NG_VALIDATORS 的令牌上的,因此在做驗證時,Angular 是注入了 NG_VALIDATORS 的依賴,也就是所有的驗證器,然后一個個的按順序執(zhí)行。因此我們這里也把自己加到這個 NG_VALIDATORS 中去。

4.但如果我們直接寫成 useExisting: RepeatValidatorDirective 會出現(xiàn)一個問題, RepeatValidatorDirective 還沒有生成,你怎么能在元數(shù)據(jù)中使用呢?這就需要使用 forwardRef 來解決這個問題,它接受一個返回一個類的函數(shù)作為參數(shù),但這個函數(shù)不會立即被調(diào)用,而是在該類聲明后被調(diào)用,也就避免了 undefined 的狀況。

下面我們就來實現(xiàn)這個驗證邏輯,由于密碼和確認密碼有主從關(guān)系,并非完全的平行關(guān)系。也就是說,密碼是一個基準對比對象,當密碼改變時,我們不應(yīng)該提示密碼和確認密碼不符,而是應(yīng)該將錯誤放在確認密碼中。所以我們給出另一個屬性 reverse 。

export class RepeatValidatorDirective implements Validator{ constructor( @Attribute('validateEqual') public validateEqual: string, @Attribute('reverse') public reverse: string) { } private get isReverse() { if (!this.reverse) return false; return this.reverse === 'true' ? true: false; } validate(c: AbstractControl): { [key: string]: any } { // 控件自身值 let self = c.value; // 要對比的值,也就是在 validateEqual=“ctrlname” 的那個控件的值 let target = c.root.get(this.validateEqual); // 不反向查詢且值不相等 if (target && self !== target.value && !this.isReverse) {  return {  validateEqual: true  } } // 反向查詢且值相等 if (target && self === target.value && this.isReverse) {  delete target.errors['validateEqual'];  if (!Object.keys(target.errors).length) target.setErrors(null); } // 反向查詢且值不相等 if (target && self !== target.value && this.isReverse) {  target.setErrors({   validateEqual: true  }) } return null; }}

這樣改造后,我們的模版文件中對于密碼和確認密碼的驗證器如下:

<input type="password" name="password" placeholder="請輸入您的密碼" [(ngModel)]="user.password" #password="ngModel" required minlength="8" validateEqual="repeat" reverse="true"><!-- 省略其他部分 --><input type="password" name="repeat" placeholder="請再次輸入密碼" [(ngModel)]="user.repeat" #repeat="ngModel" required minlength="8" validateEqual="password" reverse="false">

完成后的驗證錯誤提示

 

表單的提交

表單的提交比較簡單,綁定表單的 ngSubmit 事件即可

<form novalidate #f="ngForm" (ngSubmit)="onSubmit(f, $event)">

但需要注意的一點是,button如果不指定類型的話,會被當做 type="submit" ,所以當按鈕不是進行提交表單的話,需要顯式指定 type="button" 。而且如果遇到點擊提交按鈕頁面刷新的情況的話,意味著默認的表單提交事件引起了瀏覽器的刷新,這種時候需要阻止事件冒泡。

onSubmit({value, valid}, event: Event){  if(valid){ console.log(value); } event.preventDefault();}

對于模板驅(qū)動的表單,我們就先總結(jié)到這里,下一篇文章我們會一起討論響應(yīng)式表單。

本文代碼: https://github.com/wpcfan/ng-features.git

以上所述是小編給大家介紹的Angular 2+ 的表單(一)之模板驅(qū)動型表單,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對武林網(wǎng)網(wǎng)站的支持!

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 温泉县| 蓝山县| 天峨县| 乌什县| 兰考县| 陈巴尔虎旗| 保亭| 西青区| 馆陶县| 仁化县| 柳江县| 利津县| 东光县| 定襄县| 札达县| 长垣县| 渑池县| 宁武县| 屯昌县| 谢通门县| 攀枝花市| 苏州市| 芦溪县| 武平县| 黎城县| 仙游县| 喜德县| 辽阳县| 麻江县| 亚东县| 沈阳市| 西丰县| 页游| 阜康市| 长阳| 克什克腾旗| 噶尔县| 上虞市| 东辽县| 墨玉县| 当涂县|