[Angular學習紀錄] Component 套用 ngModel 或 formControlName

開發Angular應用程式的時候,有時候會把一些常用複合式控制元件打包成一個Component控制項,只要將商業邏輯撰寫在該控制項之後,其它開發人員若需要使用到該功能,就可以直接拿來使用了,例如下圖,是一個動態選擇電子郵件的輸入框,當資料輸入後會去檢查電子郵件的資料,並撈回可用的電子郵件。

在開發表單或者使用ngModel雙向繫結的時候,當部分控制項拉出去變成一個Component的時候,若要取得該控制項的資料,直覺的解決辦法就是使用 @Input() 和 @Output 的方式進行資料的傳遞。

但 Angular 本身就有針對 ngModel 在 Component 的資料繫結提供解決辦法,在Component 端實作 ControlValueAccessor,接下來的就來分享一下該怎麼實作該功能。


Component端說明


在開始之前,先針對要獨立出去變成 Component 功能就進行說明,我們會放置一個文字方塊,當使用者輸入資料後,會取找出適合的電子郵件下拉選單(參考上圖)讓我們選擇。

因此在HTML的配置上(關於 Angular 的事件和屬性使用方式不懂的請自行Google,本文不在解釋),程式碼會長得像底下這樣,稍微留意一下 async 的語法,這和稍後說明的後端訂閱有關連。
<div>
  <input #searchBox  (keyup)="onEmailKeyup(searchBox.value)" [value]="useEmail" placeholder="電子郵件"  />
  <!-- 我是選單區塊 -->
  <div style="position: absolute;z-index:1000">
    <div *ngFor="let item of datas | async" class="search-result" (click)="onEmaiListClick(item);" #searchList> {{item}} </div>
  </div>
</div>

後端的參考底下語法,本範例使用了RxJS來處理非同步處理事件(抓取資料),因此前端才會使用 async Pipe 。
datas: Observable<string[]>; //顯示的資料清單(RxJS中的可觀察物件)
searchSubject = new Subject<string>();  //要訂閱的事件(RxJS中的訂閱物件)
useEmail = ""; //輸入文字框

ngOnInit() {
  this.loadDataServiceSubject();
}

/**訂閱資料查詢服務 */
loadDataServiceSubject() {
  this.datas = this.searchSubject
    .debounceTime(300)        // 等候0.3秒後再執行送出
    .distinctUntilChanged()   // 忽略跟上一次一樣的輸入
    .switchMap(e => this.simulationService(e)) //查詢遠端資料
    .catch(error => { return Observable.of<string[]>([]); });
}

/**文字框 Keyup 事件 */
onEmailKeyup(value: string) {
  this.searchSubject.next(value); //執行訂閱服務
}

/**電子清單 Item Click 事件 */
onEmaiListClick(value) {
  this.useEmail = value; //設定當前文字框為選到的Item
  this.searchSubject.next(""); //清空清單
}

/**模擬查詢遠端資料服務 */
simulationService(value): Observable<string[]> {
  let result: string[] = [];
  value = value.replace("@hotmail.com", "").replace("@google.com", "").replace("@primeeagle.net", "")
  if (value !== "") {
    result.push(value + "@hotmail.com");
    result.push(value + "@google.com");
    result.push(value + "@primeeagle.net");
  }
  return Observable.of<string[]>(result);
}


Form表單使用方式


定義好Component,在表單端就可以直接使用該控制項,並將 formControlName 給予 Component,參考底下兩段的語法,仔細觀察一下我們有給予電子郵件欄位初始值,但此時初始值並沒有出現,並且選擇電子郵件清單後資料也沒有回傳。

Html端表單語法
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm.value)">
  <div><input placeholder="帳號" formControlName="account"></div>
  <!-- app-email-select 是自定義的Component -->
  <app-email-select formControlName="email"></app-email-select>
  <div><input type="submit" value="送出" /></div>
  <div>{{ myForm.value | json }}</div>
</form>

後端表單語法
export class AppComponent {
  myForm: FormGroup;
  constructor(private fb: FormBuilder) {
    this.myForm = fb.group({
      'account': [''],
      'email': ['lawrence@primeeagle.net']
    });
  }
}

宣告NG_VALUE_ACCESSOR提供者


在完成了 Component 和 Form 端的程式後,就要開始為 Component 進行改造了,首先第一個步驟要將 Component 變成一個表單控制項,Angular 本身提供了一個使用方式,定義一個常數物件,並且指定 provide 為 NG_VALUE_ACCESSOR ,還有一個 forwardRef(() => Component的類別名稱,這兩個步驟你可以理解成,將我們自定義的 Component 擴充成了一個的表單控制項。
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { forwardRef } from '@angular/core';

//步驟 1
export const USER_PROFILE_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => EmailSelectComponent),
  multi: true
};

接著再將 Component 的 providers 指向我們剛剛定義的常數物件,完成了這兩個步驟後,已經完成了一大半了~~ 沒錯 Angular 就是這麼簡單~~
@Component({
  selector: 'app-email-select',
  templateUrl: './email-select.component.html',
  providers: [USER_PROFILE_VALUE_ACCESSOR]
})


實作ControlValueAccessor介面


完成了步驟後執行一下程式會發現根本沒辦法將雙向繫結,那是因為根本就還沒有要它繫結壓,當然不會有作用,文章一開始有提到因此先來看一下 ControlValueAccessor 個這介面,該介面定義了四個方法。

  • writeValue : 用來接收資料繫結的來源端資料
  • registerOnChange : 初始化一個函數用來接收資料改變事件,用來處理資料繫結回傳資料
  • registerOnTouched : 初始化一個函數用來接收Touched事件,用於處理資料繫結回傳資料
  • setDisabledState : 當控制項的disabled屬性變更時會呼叫的方法。

由上可知,若要處理雙向資料Binding的話,必須實作 writeValue 和 registerOnChange 兩個方法,底下就是接收資料和回傳資料的範例程式碼,注意一下 onEmailListClick 事件,調整成呼叫回傳事件。
onChange: (value) => {}; //宣告一個事件
 
/**電子清單 Item Click 事件 */
onEmaiListClick(value) {
  if (this.onChange) this.onChange(value); //回傳異動資料
  this.searchSubject.next(""); //清空清單
} 

//用來接收資料繫結的來源端資料
writeValue(obj: any): void {
  this.useEmail = obj;
}

//初始化一個函數用來接收資料改變事件,用來處理資料繫結回傳資料。
registerOnChange(fn: any): void {
  this.onChange = fn;
} 




※ 本文範例使用formControlName,ngModel 的使用方式相同,請自行調整。



本文撰寫時Angular版本為v4.0。

本文範例下載 : Github,使用Angular CLI。

參考網站
[Angular進階議題]讓自訂的Component可以使用ngModel的方法




留言