Angular 2:如何在自定義控件中使用Control Value Accessor

1. 注意:angular的版本更新頻繁,本文的示例也許不能跑在最新版本的angular上,但是裡面的核心概念依然有效。

當你學習了angular2,就可以動手構建自定義表單,獲取更多幹貨芝士歡迎鎖定"朗妹兒"公眾號,每天都有新內容哦~然後你把裡面的各種控件剝離出來,準備作為獨立的組件弄到你的應用裡面去。

於是你使用ngModel和name將自定義組件插入HTML表單,然後刷新瀏覽器,打算看看效果,結果發現在瀏覽器控制檯上盡是各種亂七八糟的錯誤。

事實證明,之前的學習還遠遠不夠,我們今天就是要幫助你構建能使用ngModel特性的自定義組件。

接下來讓代碼說話。

2.注意:從Angular 2.0的RC5開始,如果要使用ngModel特徵,你需要導入模板驅動表單或響應式表單模塊。在本文後面我們會告訴你怎麼做。

示例中我們將使用最基本的input組件。你也許會說,這太簡單了,完全可以用HTML原生input元素來代替,沒錯,是這樣的。

但是我們的目的是儘量展示怎麼樣將組件暴露給接口,如果示例太過複雜,我們就很難將重點放在要講述的要點上。一旦你明白是怎麼回事,你就可以基於我們的示例修改並完成一個複雜的控件。注意,如果要完成一個能夠正常運行的控件,在背後有大量隱形的工作要做。

首先,我們看一下組件的示例代碼:

1. import { Component, forwardRef } from '@angular/core';

2. import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

3.

4. const noop = () => {

5. };

6.

7. export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {

8. provide: NG_VALUE_ACCESSOR,

9. useExisting: forwardRef(() => CustomInputComponent),

10. multi: true

11. };

12.

13. @Component({

14. selector: 'custom-input',

15. template: `

16. <label><ng-content>/<label>

17.

18. class="form-control"

19. (blur)="onBlur()" >

20.

21.

`,

22. providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]

23. })

24. export class CustomInputComponent implements ControlValueAccessor {

25.

26. //The internal data model

27. private innerValue: any = '';

28.

29. //Placeholders for the callbacks which are later providesd

30. //by the Control Value Accessor

31. private onTouchedCallback: () => void = noop;

32. private onChangeCallback: (_: any) => void = noop;

33.

34. //get accessor

35. get value(): any {

36. return this.innerValue;

37. };

38.

39. //set accessor including call the onchange callback

40. set value(v: any) {

41. if (v !== this.innerValue) {

42. this.innerValue = v;

43. this.onChangeCallback(v);

44. }

45. }

46.

47. //Set touched on blur

48. onBlur() {

49. this.onTouchedCallback();

50. }

51.

52. //From ControlValueAccessor interface

53. writeValue(value: any) {

54. if (value !== this.innerValue) {

55. this.innerValue = value;

56. }

57. }

58.

59. //From ControlValueAccessor interface

60. registerOnChange(fn: any) {

61. this.onChangeCallback = fn;

62. }

63.

64. //From ControlValueAccessor interface

65. registerOnTouched(fn: any) {

66. this.onTouchedCallback = fn;

67. }

68.

69. }

接下來看看怎麼使用這個自定義控件:

1.

預覽效果如下:

我到底看到了什麼?亮瞎我的鈦合金狗眼

不要慌,接下來我們會解釋這是怎麼回事,不過在此之前我們應該先看看兩個原則:

· 儘量直接使用HTML原生的表單元素,而不是自己實現,優先考慮原生表單元素。

· 當你需要功能更強的或者需要創建自定義表單控件的時候,請使用Angular 2提供的基礎設施。

現在開始擼一遍代碼

第一步:把代碼跑起來

因為要用ngModel和Angular 2提供的其他表單功能,我們需要導入兩個表單模塊(module)。 在本文我們使用模板驅動表單(Template Driven Forms)。

首先,我們先創建應用模塊(Application Module)。

1. //Creating our Application Module

2. import { NgModule } from '@angular/core';

3. import { BrowserModule } from '@angular/platform-browser';

4. import { FormsModule } from '@angular/forms';

5.

6. import { AppComponent } from './app.component';

7. import { CustomInputComponent } from './custom-input.component';

8.

9. @NgModule({

10. imports: [BrowserModule, FormsModule],

11. declarations: [AppComponent, CustomInputComponent],

12. bootstrap: [AppComponent]

13. })

14. export class AppModule {}

注意只要是跟瀏覽器有關的應用,都需要導入BrowserModule。由於我們還要使用ngModel這樣的表單特徵,因此還需要導入FormsModule

代碼中我們聲明瞭AppComponent,並設置為Bootstrap Component。同時我們還聲明瞭CustomInputComponent,這樣在應用中的其他地方也可以使用該組件。

配置好Application Module後,我們就可以定義應用的啟動(bootstrap)代碼。

1. import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

2. import { AppModule } from './app.module';

3.

4. platformBrowserDynamic().bootstrapModule(AppModule);

如果這段代碼看起來有困難,那說明你需要補一下官方的Angular文檔,拿去不謝。

第二步:組裝NG_VALUE_ACCESSORmulti-provider

在Angular2中可以註冊多個provider,也可以擴展已有的provider(通過擴展的方式避免重複的provider衝突)。我們需要把我們自定義的信息告知NG_VALUE_ACCESSOR令牌(Angular2 Token):我們的代碼要進行數據綁定。

1. export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {

2. provide: NG_VALUE_ACCESSOR,

3. useExisting: forwardRef(() => CustomInputComponent),

4. multi: true

5. };

上面的代碼通知了系統我們的組件是什麼。其中最關鍵的是我們使用了forwardRef,這個的作用是在註冊provider的時候,我們的組件類還沒定義(defined)好,我們需要通知Provider構造器,請等待組件類完成定義。

1. 譯者注:這段比較難理解,大概意思是在Provider和Component互相依賴的情況下,其中一方需要等待另外一方完成後再開始自己這方的初始化。

定義好了Provider後,接下來就是聲明Provider,這樣組件才能使用Provider。

1. providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]

現在Angular 2知道我們的表單控件類了,不過事情還沒完。

第三步:實現ControlValueAccessor - 關鍵點

在自定義組件裡面我們需要管理數據和事件,因為我們需要實現Angular 2提供的ControlValueAccessor接口。

代碼示例如下(實現接口,完成和數據模型的協作):

1. //The internal data model

2. private innerValue: any = '';

3.

4. //Placeholders for the callbacks which are later provided

5. //by the Control Value Accessor

6. private onTouchedCallback: () => void = noop;

7. private onChangeCallback: (_: any) => void = noop;

8.

9. //get accessor

10. get value(): any {

11. return this.innerValue;

12. };

13.

14. //set accessor including call the onchange callback

15. set value(v: any) {

16. if (v !== this.innerValue) {

17. this.innerValue = v;

18. this.onChangeCallback(v);

19. }

20. }

21.

22. //Set touched on blur

23. onBlur() {

24. this.onTouchedCallback();

25. }

26.

27. //From ControlValueAccessor interface

28. writeValue(value: any) {

29. if (value !== this.innerValue) {

30. this.innerValue = value;

31. }

32. }

33.

34. //From ControlValueAccessor interface

35. registerOnChange(fn: any) {

36. this.onChangeCallback = fn;

37. }

38.

39. //From ControlValueAccessor interface

40. registerOnTouched(fn: any) {

41. this.onTouchedCallback = fn;

42. }

首先我們先看一下最後三個方法。ControlValueAccessor接口定義的這三個方法是我們的組件內部和組件外部進行交流的口子,我們需要在代碼中實現這種交互。

writeValue方法將來自外部的數據寫入到內部的數據模型,例如:你用ngModel將控件和數據綁定。

registerOnChange方法接受一個回調方法,在數據改變的時候,你可以通過調用這個回調方法通知外部數據變了。同時你可以將修改後的數據(譯者注:可以但是不是必須)作為參數傳遞出去。

registerOnTouched方法接受一個回調方法,當你想把控件的狀態改變為touched的時候可以調用這個方法。通過Angular 2給DOM元素標籤添加正確的touched狀態和類。

注意:Angular2會提供並自動註冊這些回調功能,但是在此之前我們需要提供一個空實現(保證代碼可運行並且不出錯)。

看完這三個函數,接下來輪到組件內部代碼了。

· 回調函數佔位(空的實現,在運行時被Angular2替代)

· 內部數據的getter和setter

· 捕獲內部控件的blur事件的函數,並將整個組件的狀態設置為touched

這裡面並沒有什麼高科技,用心都能學會。

至此,我們的新的自定義控件支持ngControl和ngModel指令了。

騷年,動起來吧。

如果你還想繼續提升,掌握Angular2更牛的表單控件,那就去研究一下Angular Meterial 2的源碼,和本文主題有關的問題都能在裡面找到答案。


分享到:


相關文章: