angulartypescriptangular-materialdatepickermat-datepicker

Angular Material disable Month


I'm using my Angular project for Angular material Year Month picker. Anyone has some idea how to show only Year for the pick.

stackblitz here

.html

<mat-form-field appearance="fill">
  <mat-label>Month and Year</mat-label>
  <input matInput [matDatepicker]="dp" [formControl]="date">
  <mat-datepicker-toggle matSuffix [for]="dp"></mat-datepicker-toggle>
  <mat-datepicker #dp
                  startView="multi-year"
                  (yearSelected)="chosenYearHandler($event)"
                  (monthSelected)="chosenMonthHandler($event, dp)"
                  panelClass="example-month-picker">
  </mat-datepicker>
</mat-form-field>

.ts

import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import {MomentDateAdapter, MAT_MOMENT_DATE_ADAPTER_OPTIONS} from '@angular/material-moment-adapter';
import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE} from '@angular/material/core';
import {MatDatepicker} from '@angular/material/datepicker';

// Depending on whether rollup is used, moment needs to be imported differently.
// Since Moment.js doesn't have a default export, we normally need to import using the `* as`
// syntax. However, rollup creates a synthetic default module and we thus need to import it using
// the `default as` syntax.
import * as _moment from 'moment';
// tslint:disable-next-line:no-duplicate-imports
import {default as _rollupMoment, Moment} from 'moment';

const moment = _rollupMoment || _moment;

// See the Moment.js docs for the meaning of these formats:
// https://momentjs.com/docs/#/displaying/format/
export const MY_FORMATS = {
  parse: {
    dateInput: 'MM/YYYY',
  },
  display: {
    dateInput: 'MM/YYYY',
    monthYearLabel: 'MMM YYYY',
    dateA11yLabel: 'LL',
    monthYearA11yLabel: 'MMMM YYYY',
  },
};

/** @title Datepicker emulating a Year and month picker */
@Component({
  selector: 'datepicker-views-selection-example',
  templateUrl: 'datepicker-views-selection-example.html',
  styleUrls: ['datepicker-views-selection-example.css'],
  providers: [
    // `MomentDateAdapter` can be automatically provided by importing `MomentDateModule` in your
    // application's root module. We provide it at the component level here, due to limitations of
    // our example generation script.
    {
      provide: DateAdapter,
      useClass: MomentDateAdapter,
      deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS]
    },

    {provide: MAT_DATE_FORMATS, useValue: MY_FORMATS},
  ],
})
export class DatepickerViewsSelectionExample {
  date = new FormControl(moment());

  chosenYearHandler(normalizedYear: Moment) {
    const ctrlValue = this.date.value;
    ctrlValue.year(normalizedYear.year());
    this.date.setValue(ctrlValue);
  }

  chosenMonthHandler(normalizedMonth: Moment, datepicker: MatDatepicker<Moment>) {
    const ctrlValue = this.date.value;
    ctrlValue.month(normalizedMonth.month());
    this.date.setValue(ctrlValue);
    datepicker.close();
  }
}

Solution

  • Concept (In short)

    1. Set MY_FORMATS dateInput as YYYY to display year only.
    2. Glue: Add providers for NG_VALUE_ACCESSOR and the multi-provider to extend existing providers. (Refer to References 2)
    3. Machinery: Implement ControlValueAccessor to the component to integrate the custom form control value with Angular Reactive Forms. (Refer to References 2 & 3)

    Solution

    datepicker-views-selection-example.html

    <mat-form-field appearance="fill">
      <mat-label>Month and Year</mat-label>
      <input matInput
            [matDatepicker]="dp"
            [formControl]="date"
             (click)="_openDatepickerOnClick(dp)"
             (keydown.arrowdown)="_openDatepickerOnClick(dp)"
             (keydown.enter)="_openDatepickerOnClick(dp)">
      <mat-datepicker-toggle matSuffix [for]="dp"></mat-datepicker-toggle>
      <mat-datepicker #dp 
                      startView="multi-year" 
                      (yearSelected)="chosenYearHandler($event, dp)"
                      (monthSelected)="chosenMonthHandler($event, dp)"
                      panelClass="example-month-picker">
      </mat-datepicker>
    </mat-form-field>
    

    datepicker-views-selection-example.ts

    import { Component, forwardRef, Input, ViewChild } from '@angular/core';
    import {
      ControlValueAccessor,
      FormControl,
      NG_VALUE_ACCESSOR
    } from '@angular/forms';
    import {
      MomentDateAdapter,
      MAT_MOMENT_DATE_ADAPTER_OPTIONS
    } from '@angular/material-moment-adapter';
    import {
      DateAdapter,
      MAT_DATE_FORMATS,
      MAT_DATE_LOCALE
    } from '@angular/material/core';
    import { MatDatepicker } from '@angular/material/datepicker';
    
    // Depending on whether rollup is used, moment needs to be imported differently.
    // Since Moment.js doesn't have a default export, we normally need to import using the `* as`
    // syntax. However, rollup creates a synthetic default module and we thus need to import it using
    // the `default as` syntax.
    import * as _moment from 'moment';
    // tslint:disable-next-line:no-duplicate-imports
    import { default as _rollupMoment, Moment } from 'moment';
    
    const moment = _rollupMoment || _moment;
    
    // See the Moment.js docs for the meaning of these formats:
    // https://momentjs.com/docs/#/displaying/format/
    export const MY_FORMATS = {
      parse: {
        dateInput: 'YYYY'
      },
      display: {
        dateInput: 'YYYY',
        monthYearLabel: 'MMM YYYY',
        dateA11yLabel: 'LL',
        monthYearA11yLabel: 'MMMM YYYY'
      }
    };
    
    /** @title Datepicker emulating a Year and month picker */
    @Component({
      selector: 'datepicker-views-selection-example',
      templateUrl: 'datepicker-views-selection-example.html',
      styleUrls: ['datepicker-views-selection-example.css'],
      providers: [
        // `MomentDateAdapter` can be automatically provided by importing `MomentDateModule` in your
        // application's root module. We provide it at the component level here, due to limitations of
        // our example generation script.
        {
          provide: DateAdapter,
          useClass: MomentDateAdapter,
          deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS]
        },
        { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
        {
          provide: NG_VALUE_ACCESSOR,
          useExisting: forwardRef(() => DatepickerViewsSelectionExample),
          multi: true
        }
      ]
    })
    export class DatepickerViewsSelectionExample implements ControlValueAccessor {
      @ViewChild('dp') dp: MatDatepicker<Moment>;
    
      date = new FormControl(moment());
    
      _max: Moment | undefined;
      @Input()
      get max(): Moment | undefined {
        return this._max ? this._max : undefined;
      }
      set max(max: Moment | undefined) {
        if (max) {
          const momentDate: Moment =
            typeof max === 'number' ? moment([max, 0, 1]) : moment(max);
          this._max = momentDate.isValid() ? momentDate : undefined;
        }
      }
    
      _min: Moment | undefined;
      @Input()
      get min(): Moment | undefined {
        return this._min ? this._min : undefined;
      }
      set min(min: Moment | undefined) {
        if (min) {
          const momentDate =
            typeof min === 'number' ? moment([min, 0, 1]) : moment(min);
          this._min = momentDate.isValid() ? momentDate : undefined;
        }
      }
    
      // Function to call when the date changes.
      onChange = (year: Date) => {};
    
      // Function to call when the input is touched (when a star is clicked).
      onTouched = () => {};
    
      writeValue(date: Date): void {
        if (date && this._isYearEnabled(date.getFullYear())) {
          const momentDate = moment(date);
          if (momentDate.isValid()) {
            momentDate.set({ date: 1 });
            this.date.setValue(moment(date), { emitEvent: false });
          }
        }
      }
    
      registerOnChange(fn: any): void {
        this.onChange = fn;
      }
    
      registerOnTouched(fn: any): void {
        this.onTouched = fn;
      }
    
      // Allows Angular to disable the input.
      setDisabledState(isDisabled: boolean): void {
        isDisabled ? (this.dp.disabled = true) : (this.dp.disabled = false);
    
        isDisabled ? this.date.disable() : this.date.enable();
      }
    
      chosenYearHandler(normalizedYear: Moment, datepicker: MatDatepicker<Moment>) {
        datepicker.close();
    
        if (!this._isYearEnabled(normalizedYear.year())) {
          return;
        }
    
        normalizedYear.set({ date: 1 });
    
        this.date.setValue(normalizedYear, { emitEvent: false });
        this.onTouched();
      }
    
      chosenMonthHandler(
        normalizedMonth: Moment,
        datepicker: MatDatepicker<Moment>
      ) {
        datepicker.close();
    
        this.date.setValue(normalizedMonth.month());
        this.onTouched();
      }
    
      _openDatepickerOnClick(datepicker: MatDatepicker<Moment>) {
        if (!datepicker.opened) {
          datepicker.open();
        }
      }
    
      // disable if the year is greater than maxDate lower than minDate
      private _isYearEnabled(year: number) {
        if (
          year === undefined ||
          year === null ||
          (this._max && year > this._max.year()) ||
          (this._min && year < this._min.year())
        ) {
          return false;
        }
    
        return true;
      }
    }
    

    Sample Solution on StackBlitz


    References

    1. @Tushar's answer on How to select only year on mat-datepicker
    2. Connect your custom control to ngModel with Control Value Accessor.
    3. ControlValueAccessor