core114
core114

Reputation: 5335

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();
  }
}

Upvotes: 1

Views: 2414

Answers (1)

Yong Shun
Yong Shun

Reputation: 51440

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

Upvotes: 1

Related Questions