在 Angular 應用中,咱們有兩種方式來實現表單綁定——「模板驅動表單」與「響應式表單」。這兩種方式一般可以很好的處理大部分的狀況,可是對於一些特殊的表單控件,例如input[type=datetime]
、input[type=file]
,咱們須要重寫默認的表單綁定方式,讓咱們綁定的變量再也不僅僅只是一個字符串,而是一個 Date
或者 File
對象。爲了達成這一目的,咱們須要自定義表單控件的 ControlValueAccessor
。html
ControlValueAccessor
接口是 Angular Forms API 與 DOM 之間的橋樑,經過提供不一樣的 ControlValueAccessor
,咱們就可使用統一的 Angular Forms API 來操做不一樣的 HTML 表單元素。前端
在咱們使用 ngModel
或者 formControl
的時候,這兩個 Directive 會向 Angular 的依賴注入容器申請實現了 ControlValueAccessor
接口的對象,這是一種典型的面向接口編程的設計。例如,若是咱們須要爲 input[type=file]
提供一個用來綁定 File
對象的 ControlValueAccessor
,只須要在依賴注入容器中提供一個 FileControlValueAccessor
的實現就能夠了。不過,咱們並不想覆蓋其餘類型 input
元素的 ControlValueAccessor
,由於那樣確定會對已有代碼形成大範圍的破壞。因此在這裏,咱們須要使用 Angular 的分層注入能力——在 ElementInjector 中提供 FileControlValueAccessor
。關於 ElementInjector 更多的內容,請看這裏 a-curios-case-of-the-host-decorator-and-element-injectors-in-angular。ios
下面演示的兩個 Directive 您均可以在這裏查看在線演示。typescript
首先讓咱們來建立一個 Directive,這個指令將會選中 input[type=file][appInputFile]
元素,這樣咱們就能夠有選擇的爲文件選擇器的 ElementInjector 定義新的 Provider。編程
@Directive({ selector: 'input[type=file][inputFile]', // <1> providers: [ { provide: NG_VALUE_ACCESSOR, // <2> useExisting: forwardRef(() => InputFileDirective), // <3> multi: true // <4> } ] }) export class InputFileDirective implements ControlValueAccessor, OnInit, OnDestroy { // 當文件選擇器選擇的文件發生改變時調用的回調函數 onChange: (any) => any; // 當文件選擇器選擇的被操做後調用的回調函數 onTouched: () => any; // 監聽宿主元素的 change 事件 @HostListener('change', ['$event.target.files']) onElChange = (files: FileList) => { this.onChange(files); }; // 監聽宿主元素的 blur 事件 @HostListener('blur', []) onElTouched = () => { this.onTouched(); }; constructor(private el: ElementRef<HTMLInputElement>) { // <5> } ngOnInit(): void { this.el.nativeElement.addEventListener('change', this.listener); } // 來自 ControlValueAccessor 接口,用來設置元素的值 writeValue(obj: any): void { this.el.nativeElement.value = obj; } // 來自 ControlValueAccessor 接口,用來將一個函數註冊爲 onChange 回調函數 registerOnChange(fn: any): void { this.onChange = fn; } // 來自 ControlValueAccessor 接口,用來將一個函數註冊爲 onTouched 回調函數 registerOnTouched(fn: any): void { this.onTouched = fn; } // 來自 ControlValueAccessor 接口,設置表單元素是否啓用 setDisabledState?(isDisabled: boolean): void { this.el.nativeElement.disabled = isDisabled; } }
上面的代碼片斷中你能夠看到有幾處相似 // <1>
的註釋,這是我用來在下面的文章中引用該行代碼的標記,語法借鑑自 ASCIIDocapi
input[type=file]
重寫 ControlValueAccessor
ControlValueAccessor
的注入 token 是一個常量 —— NG_VALUE_ACCESSOR
forwardRef
來引用這個依賴的實現。ControlValueAccessor
就是提供了多個實現的。在解析依賴的時候,Angular 會優先選擇咱們自定義的實現。ElementRef.nativeElement
來讀取原生 HTML 元素的屬性,若是你對服務端渲染有需求,你應該使用 Renderer2
來讀寫元素的屬性。有了這個 Directive,咱們就能夠在 Angular Forms 中綁定 File 對象了:app
<input type="file" [(ngModel)]="foo.files" inputFile />
Date
類型的數據也是平常開發中比較頭疼的一個地方,由於在 JSON 中,Date
類型每每會被序列化爲字符串,而在前端代碼中,咱們又須要將其反序列化爲 Date
對象,最終在頁面上展現的時候,咱們又須要按照產品需求再將其序列化爲制定格式的字符串。如今,有了 ControlValueAccessor
的幫助,咱們就能夠實現讓 input[type=datetime]
與 Date
對象進行雙向綁定的功能,同時還可以定製 Date 對象在輸入框中的顯示格式。ide
@Directive({ // tslint:disable-next-line:directive-selector selector: 'input[type=datetime][valueAsDate]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateValueDirective), multi: true } ] }) export class DateValueDirective implements ControlValueAccessor { /** * See https://date-fns.org/v2.0.0-alpha.25/docs/format * 自定義日期展現格式 * @type {string} * @memberof DateValueDirective */ // tslint:disable-next-line:no-input-rename @Input('valueAsDate') format: string; private dateValue: Date; @HostListener('input', ['$event.target.value']) onChange = (_: any) => { }; @HostListener('blur', []) onTouched = () => { }; get element() { return this.elementRef.nativeElement; } constructor( private elementRef: ElementRef, private renderer: Renderer2 // <1> ) { } parseDate(str: string) { return parseDate(str, this.format, new Date(), { awareOfUnicodeTokens: true }); } formatDate(date: Date) { return formatDate(date, this.format, { awareOfUnicodeTokens: true }); } /** * 設置組件的值的時候,先把新的值存到一個成員變量中,而後再把新的值格式化爲 string */ writeValue(date: Date): void { this.dateValue = date; this.renderer.setProperty(this.element, 'value', this.formatDate(date)); } /** * 在 input 元素值發生變化的時候,先嚐試把變化後的值轉換成 Date 對象 * 若是轉換失敗,那麼依然使用以前的值 * 不然,將新的值傳遞給回調函數 */ registerOnChange(fn: any): void { const onChange = (value: string) => { const date = this.parseDate(value); if (isValidDate(date)) { this.dateValue = date; fn(date); } else { fn(this.dateValue); } }; this.onChange = onChange; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState?(isDisabled: boolean): void { this.renderer.setProperty(this.element, 'disabled', isDisabled); } }
Renderer2
來讀寫元素屬性的操做整個指令的內容仍然很是簡單,可是卻可以爲咱們的平常開發帶來不小的便利,使用了這個指令後,咱們就能夠很是容易的爲 Date 對象進行雙向綁定。函數
<input type="datetime" valueAsDate="M/d/yyyy h:mm:ss a" [(ngModel)]="foo.date">