Customise mat datepicker header


Posted by tsungtingdu on 2021-07-09

在 Angular 當中使用 mat-datepicker 的時候,可以透過建立自己的 header component 來客製化 datepicker 的使用體驗。譬如改變不同頁面顯示的 label,以及頁面當中 buttons 的功能等等。

In order to interact with the calendar in your custom header component, you can inject the parent MatCalendar in the constructor. To make sure your header stays in sync with the calendar, subscribe to the stateChanges observable of the calendar and mark your header component for change detection. --ref

Example (ref):

首先建立 DatePickerHeaderComponent

import {
  MatCalendar, 
  MatCalendarView,
  MatDatepickerIntl,
  yearsPerPage
} from '@angular/material/datepicker';

@Component({
  selector: 'xxx',
  templateUrl: 'xxx',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DatePickerHeaderComponent<D> implements OnDestory {
  private _destroyed = new Subject<void>();

  constructor(
    private intl: MatDatepickerIntl,
    private matCalendar: MatCalendar<D>,
    private dateAdapter: DateAdapter<D>,
    cdr: ChangeDetectorRef) {
    matCalendar.stateChanges
      .pipe(takeUntil(this._destroyed))
      .subscribe(() => cdr.markForCheck());
  }

  ngOnDestroy() {
    this._destroyed.next();
    this._destroyed.complete();
  }
}

在這裡因為我們 inject 了 MatCalendar,所以我們可以 access 到一些原本在 mat-datepicker 裡面無法接觸到的 attributes & methods,像是

  • selectedChange: 最終的 selected date。在 mat-datepicker 上只能拿到 monthSelected 以及 yearSelected
  • set currentView: 可以決定要換到 multi-year, year, 或是 month 頁面
  • set activeDate: 決定當前的日期,以決定 currentView 當中需要出現什麼範圍的日期資料

更多 api 可以參考這裡


接著我們就可以在 parent component 引入 DatePickerHeaderComponent:

export class ExampleComponent {
  datepickerHeader = DatePickerHeaderComponent;
}
<mat-form-field>
  <input matInput [matDatepicker]="picker" [formControlName]="xxx"/>
</mat-form-field>
<mat-datepicker #picker [calendarHeaderComponent]="datepickerHeader">

在測試方面,需要創造一個 wrapper component,然後測試前需要開啟 datepicker。在我最近的專案當中,測試方式是直接使用 dom 操作來模擬使用者操作 datepickerHeader,不確定有沒有更好的測試方式。

通常不知道怎麼寫測試的時候,我會參考其他專案的寫法,或者,就直接看原始碼的測試。譬如這裡的測試寫法,就是參考原始碼當中測試的寫法。

@Component({
  template: `
    <input [matDatepicker]="ch">
    <mat-datepicker #ch startView="multi-year" [calendarHeaderComponent]="datepickerHeader"></mat-datepicker>
  `
})
class DatePickerHeaderWrapperComponent {
  @ViewChild('ch') datepicker: MatDatepicker<Date>;
  datepickerHeader = DatePickerHeaderComponent;
}

describe('datepicker with custom header', () => {
  let fixture: ComponentFixture<DatePickerHeaderWrapperComponent>;
  let testComponent: DatePickerHeaderWrapperComponent;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule,
        MatDatepickerModule,
        MatFormFieldModule,
        MatInputModule,
        NoopAnimationsModule,
        ReactiveFormsModule,
        MatNativeDateModule
      ],
      declarations: [DatePickerHeaderWrapperComponent, DatePickerHeaderComponent],
      schemas: [NO_ERRORS_SCHEMA]
    });
    TestBed.overrideModule(BrowserDynamicTestingModule, {
      set: {
        entryComponents: [DatePickerHeaderComponent]
      }
    }).compileComponents();

    fixture = TestBed.createComponent(DatePickerHeaderWrapperComponent);
    fixture.detectChanges();
    testComponent = fixture.componentInstance;
    testComponent.datepicker.open();
    fixture.detectChanges();
  });

  ...
})

最後來講一下本週開發遇到的問題。

在 parent component 當中,我有使用到 mat-datepicker-action,也就是說,使用者在選完日期之後,需要按下 "Apply" 才能夠將日期真正送到 form 裡面。如果沒有使用 mat-datepicker-action,使用者選完日期之後,datepicker 會自動關閉,然後將資料送給 form。

不過不管使用者有沒有選到日期,其實這個 "Apply" button 會一直存在,所以設計師希望能夠在使用者選完之後,才 enable button。但問題就在於剛剛提到的,從 mat-datepicker 身上我們只能拿到 monthSelected 以及 yearSelected,卻拿不到最後的 selectedChange

有想到在 DatePickerHeaderComponent 當中取得 selectedChange,然後記錄在 localStorage 當中,提供 parent component 來使用(或者透過 store 也行)。

另外一個比較 "hacky" 的做法是,檢查 month view (最後要選日期的頁面) 當中的 html elements 是否有出現 .mat-calendat-body-selected 的 class,因為這是 mat-datepicker 用來標示被選到日期的 class。

結果你猜,我最後用了哪一個呢?


#Angular #material







Related Posts

SSRF lab (1)

SSRF lab (1)

VSCode Emmet: Go to Matching Pair

VSCode Emmet: Go to Matching Pair

關於 JavaScript Promises

關於 JavaScript Promises


Comments