angularangular-materialdatepickerangular-material-datetimepickermaterialdatepicker

Angular: Material Datepicker: date range selection in month view


I am working with Angular/Material version 19 (standalone).

When I want to display the calendar in a month view, this is my template's code:

<mat-calendar #calendar
      [(selected)]="selected"
      [dateClass]="dateClass"
      (selectedChange)="onDateSelected($event)">
      </mat-calendar>

enter image description here

And everything is working fine.

When I want to display the calendar for date range selection, this is the template code:

<mat-form-field>
    <mat-label>Select a date range</mat-label>
    <mat-date-range-input [formGroup]="range" [rangePicker]="picker">
        <input matStartDate formControlName="start" placeholder="Start date" readonly />
        <input matEndDate formControlName="end" placeholder="End date" readonly />
    </mat-date-range-input>
    <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>

enter image description here

And everything is working fine with that too.

But now, I need them combined - I need to show the calendar in a month view, but for the user to select a date range and not just a single specific date.

How do I do that exactly? If that's possible, of course.


Solution

  • The month view is not working which is suspect is an angular bug, I have raised a github issue -> CALENDAR: start view month is not working hopefully it is resolved/answered.

    I found this great article on setting range selection for Mat Calendar:

    Using Angular Material's calendar with date ranges and range presets

    We should import two providers that take care of range selection.

    import {
      DateRange,
      MatDatepickerModule,
      MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER,
      DefaultMatCalendarRangeStrategy,
      MatRangeDateSelectionModel,
    } from '@angular/material/datepicker';
    ...
    
    ...
    @Component({
      selector: 'date-range-picker-forms-example',
      templateUrl: 'date-range-picker-forms-example.html',
      providers: [DefaultMatCalendarRangeStrategy, MatRangeDateSelectionModel],
    

    After this, we can just use the strategy to determine the selection needed for the date picker.

    constructor(
      private readonly selectionModel: MatRangeDateSelectionModel<Date>,
      private readonly selectionStrategy: DefaultMatCalendarRangeStrategy<Date>
    ) {}
    
    // Event handler for when the date range selection changes.
    rangeChanged(selectedDate: Date, callback: Function) {
      const selection = this.selectionModel.selection,
        newSelection = this.selectionStrategy.selectionFinished(
          selectedDate,
          selection
        );
    
      this.selectionModel.updateSelection(newSelection, this);
    

    But we also need the form to be updated with the latest values, for this, we need to introduce a callback, which updates the form, but the selectedChange is executed inside the datepicker, so that form of the component is not visible to it, so we use .bind(this) to make sure that when the callback get's executed it has access to the component form.

    <mat-calendar
      [selected]="this.selectedDateRange"
      [comparisonStart]="this.selectedDateRange!.start"
      [comparisonEnd]="this.selectedDateRange!.end"
      (selectedChange)="this.rangeChanged($event, setFormRangeControls.bind(this))"
    ></mat-calendar>
    

    So the final callback sets the form values.

    setFormRangeControls() {
      this.range.setValue({
        start: this.selectedDateRange?.start || null,
        end: this.selectedDateRange?.end || null,
      });
    }
    

    HTML:

    <mat-form-field>
      <mat-label>Enter a date range</mat-label>
      <mat-date-range-input [formGroup]="range">
        <input matStartDate formControlName="start" placeholder="Start date" />
        <input matEndDate formControlName="end" placeholder="End date" />
      </mat-date-range-input>
      <mat-hint>MM/YYYY – MM/YYYY</mat-hint>
      @if (range.controls.start.hasError('matStartDateInvalid')) {
      <mat-error>Invalid start date</mat-error>
      } @if (range.controls.end.hasError('matEndDateInvalid')) {
      <mat-error>Invalid end date</mat-error>
      }
    </mat-form-field>
    <mat-calendar
      [selected]="this.selectedDateRange"
      [comparisonStart]="this.selectedDateRange!.start"
      [comparisonEnd]="this.selectedDateRange!.end"
      (selectedChange)="this.rangeChanged($event, setFormRangeControls.bind(this))"
    ></mat-calendar>
    

    TS:

    import { JsonPipe } from '@angular/common';
    import { ChangeDetectionStrategy, Component } from '@angular/core';
    import {
      FormControl,
      FormGroup,
      FormsModule,
      ReactiveFormsModule,
    } from '@angular/forms';
    import { provideNativeDateAdapter } from '@angular/material/core';
    import {
      DateRange,
      MatDatepickerModule,
      MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER,
      DefaultMatCalendarRangeStrategy,
      MatRangeDateSelectionModel,
    } from '@angular/material/datepicker';
    import { MatFormFieldModule } from '@angular/material/form-field';
    
    import * as _moment from 'moment';
    // tslint:disable-next-line:no-duplicate-imports
    import { default as _rollupMoment, Moment } from 'moment';
    import { MatInputModule } from '@angular/material/input';
    import { provideMomentDateAdapter } from '@angular/material-moment-adapter';
    
    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 Date range picker forms integration */
    @Component({
      selector: 'date-range-picker-forms-example',
      templateUrl: 'date-range-picker-forms-example.html',
      providers: [DefaultMatCalendarRangeStrategy, MatRangeDateSelectionModel],
      imports: [
        MatFormFieldModule,
        MatDatepickerModule,
        FormsModule,
        ReactiveFormsModule,
        JsonPipe,
      ],
      changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class DateRangePickerFormsExample {
      readonly range = new FormGroup({
        start: new FormControl<Date | null>(null),
        end: new FormControl<Date | null>(null),
      });
      selectedDateRange: DateRange<Date | null> | undefined = new DateRange(
        null,
        null
      );
    
      constructor(
        private readonly selectionModel: MatRangeDateSelectionModel<Date>,
        private readonly selectionStrategy: DefaultMatCalendarRangeStrategy<Date>
      ) {}
    
      // Event handler for when the date range selection changes.
      rangeChanged(selectedDate: Date, callback: Function) {
        const selection = this.selectionModel.selection,
          newSelection = this.selectionStrategy.selectionFinished(
            selectedDate,
            selection
          );
    
        this.selectionModel.updateSelection(newSelection, this);
        // sync the selection the form controls
        this.selectedDateRange = new DateRange<Date>(
          newSelection.start,
          newSelection.end
        );
        if (callback) {
          callback();
        }
      }
    
      setFormRangeControls() {
        this.range.setValue({
          start: this.selectedDateRange?.start || null,
          end: this.selectedDateRange?.end || null,
        });
      }
    }
    

    Stackblitz Demo