[Angular學習紀錄] Component 套用 ngModel 或 formControlName
開發Angular應用程式的時候,有時候會把一些常用複合式控制元件打包成一個Component控制項,只要將商業邏輯撰寫在該控制項之後,其它開發人員若需要使用到該功能,就可以直接拿來使用了,例如下圖,是一個動態選擇電子郵件的輸入框,當資料輸入後會去檢查電子郵件的資料,並撈回可用的電子郵件。
在開發表單或者使用ngModel雙向繫結的時候,當部分控制項拉出去變成一個Component的時候,若要取得該控制項的資料,直覺的解決辦法就是使用 @Input() 和 @Output 的方式進行資料的傳遞。
但 Angular 本身就有針對 ngModel 在 Component 的資料繫結提供解決辦法,在Component 端實作 ControlValueAccessor,接下來的就來分享一下該怎麼實作該功能。
在開始之前,先針對要獨立出去變成 Component 功能就進行說明,我們會放置一個文字方塊,當使用者輸入資料後,會取找出適合的電子郵件下拉選單(參考上圖)讓我們選擇。
因此在HTML的配置上(關於 Angular 的事件和屬性使用方式不懂的請自行Google,本文不在解釋),程式碼會長得像底下這樣,稍微留意一下 async 的語法,這和稍後說明的後端訂閱有關連。
後端的參考底下語法,本範例使用了RxJS來處理非同步處理事件(抓取資料),因此前端才會使用 async Pipe 。
定義好Component,在表單端就可以直接使用該控制項,並將 formControlName 給予 Component,參考底下兩段的語法,仔細觀察一下我們有給予電子郵件欄位初始值,但此時初始值並沒有出現,並且選擇電子郵件清單後資料也沒有回傳。
Html端表單語法
後端表單語法
在完成了 Component 和 Form 端的程式後,就要開始為 Component 進行改造了,首先第一個步驟要將 Component 變成一個表單控制項,Angular 本身提供了一個使用方式,定義一個常數物件,並且指定 provide 為 NG_VALUE_ACCESSOR ,還有一個 forwardRef(() => Component的類別名稱,這兩個步驟你可以理解成,將我們自定義的 Component 擴充成了一個的表單控制項。
接著再將 Component 的 providers 指向我們剛剛定義的常數物件,完成了這兩個步驟後,已經完成了一大半了~~ 沒錯 Angular 就是這麼簡單~~
完成了步驟後執行一下程式會發現根本沒辦法將雙向繫結,那是因為根本就還沒有要它繫結壓,當然不會有作用,文章一開始有提到因此先來看一下 ControlValueAccessor 個這介面,該介面定義了四個方法。
由上可知,若要處理雙向資料Binding的話,必須實作 writeValue 和 registerOnChange 兩個方法,底下就是接收資料和回傳資料的範例程式碼,注意一下 onEmailListClick 事件,調整成呼叫回傳事件。
※ 本文範例使用formControlName,ngModel 的使用方式相同,請自行調整。
本文撰寫時Angular版本為v4.0。
本文範例下載 : Github,使用Angular CLI。
參考網站
[Angular進階議題]讓自訂的Component可以使用ngModel的方法
在開發表單或者使用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的方法
留言
張貼留言
您好,我是 Lawrence,這裡是我的開發筆記的網誌,如果你對我的文章有任何疑問或者有錯誤的話,歡迎留言讓我知道。