用 ForwardRef 解決在 Angular 中遇到的 bug

因職位轉換為全端,所以重新學習新的前端框架 angular。公司前端架構目前是 angular js 和 angular 混雜使用,雖然短期內可以順利運作,但長期還是得全部轉換至 angular 才好維護。剛好轉換的工作就讓我這 angular 菜鳥來練練手啦!

這次遇到的 Bug 如下,這是 code base 中已經寫好的 angular directive

@Directive({
  selector: '[datePicker]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: DatePickerDirective,
      multi: true,
    },
  ],
})
export class DatePickerDirective implements OnInit, ControlValueAccessor, OnChanges {
  @Input() .....

簡單來說他把 jQuery 的 date picker 套件包成 directive,並宣告成 NG_VALUE_ACCESSOR,這樣就能使用 ngModel 來做綁定

<input type="text"
       datePicker  
       [(ngModel)]="asOfDate"
       class="form-control form-date">                                                

但實際測試,發現點擊 input 時 date picker 根本不會出現,為了確定 directive 有效,我在 ngOnInit 的部分加了 log

...
ngOnInit(): void {
  console.log("date-picker work!");
...
}

發現這一行根本不會跑!非常的奇怪,由於同一份檔案還有其他類似包裝 jQuery 的 directive,觀察了一下,另一個用過且確定可以動的 directive 並沒有把他宣告成 NG_VALUE_ACCESSOR,看來這個可能是鬼,於是把他註解掉

@Directive({
  selector: '[datePicker]',
  //providers: [
  //  {
  //    provide: NG_VALUE_ACCESSOR,
  //    useExisting: DatePickerDirective,
  //    multi: true,
  //  },
  ],
})
export class DatePickerDirective implements OnInit, ControlValueAccessor, OnChanges {
  @Input() .....

再試一次,date picker 可以正常出現!但日期卻無法綁定,這合理,因為我們拔掉了 NG_VALUE_ACCESSOR。

解法是,在 useExisting 參數使用 fowardRef 來傳遞 class 實體,而不直接傳 class 進去,如下

@Directive({
  selector: '[datePicker]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DatePickerDirective),
      multi: true,
    },
  ],
})
export class DatePickerDirective implements OnInit, ControlValueAccessor, OnChanges {
  @Input() .....

主因是我們在 decorator 裡面引用 DatePickerDirective 時,他順序還在後面沒有宣告到,可能因此導致 angular 出錯,轉變成我們觀察到的現象: directive 根本沒有執行。

但 angular 框架規定要把 NG_VALUE_ACCESSOR 相關的定義放在 decorator 裡面的 providers,此時只好使用 forwardRef 函數作為中介,讓他先取到 forwardRef 函數實體,之後 angular 內部要使用時就可以透過它回傳已經走過宣告的 DatePickerDirective class 實體,如此即能解決宣告順序的問題。

參考資料

[Angular 大師之路] Day 08 – 自訂表單控制項
Forward references in Angular

Written by J
雖然大學唸的是生物,但持著興趣與熱情自學,畢業後轉戰硬體工程師,與宅宅工程師們一起過著沒日沒夜的生活,做著台灣最薄的 intel 筆電,要與 macbook air 比拼。 離開後,憑著一股傻勁與朋友創業,再度轉戰軟體工程師,一手扛起前後端、雙平台 app 開發,過程中雖跌跌撞撞,卻也累計不少經驗。 可惜不是那 1% 的成功人士,於是加入其他成功人士的新創公司,專職開發後端。沒想到卻在採前人坑的過程中,拓寬了眼界,得到了深層的領悟。