1

I have a requirement that user can select multiple dates in datepicker, but after each date selection the mat-datepicker closes, I want to prevent mat-datepicker from closing on date selection and close the datepicker when we click outside the datepicker or on calendar icon.

datepicker

I tried closed event on mat-datepicker but the date-picker is flickering on each date selection plus it is doesn't close when we click outside the datepicker.

Here's what I tried:

html code:

<input formControlName="fromDate" matInput [matDatepicker]="fromDatePicker" placeholder="From Date" [min]="minDate" readonly>
<mat-datepicker-toggle matSuffix [for]="fromDatePicker"></mat-datepicker-toggle>
<mat-datepicker #fromDatePicker (closed)="_openCalendar(fromDatePicker)"></mat-datepicker>

typescript code:

_openCalendar(picker: MatDatepicker<Date>) {
    picker.open();
}
Shreyas Pednekar
  • 1,285
  • 5
  • 31
  • 53
  • 1
    https://stackoverflow.com/questions/59133977/multiple-date-select-in-material-datepicker-angular – Eliseo Dec 02 '19 at 14:33

3 Answers3

4

The way I prevented the overlay to close was to change MatCalendar._userSelected method.

If you look into the template of MatDatePickerContent, you would notice the call for when _userSelection is emitting. The call is to close the overlay: (_userSelection)="datepicker.close()". Now, _userSelection is emitting in MatCalendar._userSelected.

datepicker.js, line 1687, "@angular/material": "8.2.3":

_userSelected() {
    this._userSelection.emit();
}

Because I did not wanted this behaviour for all the instances of DatePicker in the application, in my component I did as follow:

@ViewChild('picker', { static: true }) 
private picker: MatDatepicker<Date>;
private preventCloseOnSelection = true;
private readonly initCalendarUserSelected: () => void;
public model: Array<Date>;

constructor(private readonly cdr: ChangeDetectorRef) {
    this.initCalendarUserSelected = MatCalendar.prototype._userSelected;
}

public ngAfterViewInit() {
    this.picker.calendarHeaderComponent = CalendarTimeHeaderComponent;

    this.picker.openedStream
        .pipe(takeUntil(this.isUnsubscribing))
        .subscribe(() => {

            if (this.preventCloseOnSelection) {
                MatCalendar.prototype._userSelected = () => {};
            } else {
                MatCalendar.prototype._userSelected = this.initCalendarUserSelected;
            }

            this.cdr.detectChanges();
        });
}

public ngOnDestroy() {
    MatCalendar.prototype._userSelected = this.initCalendarUserSelected;
}

If you want this behaviour for all the instances of date picker, you can override the MatCalendar._userSelected method somewhere in AppComponent and not bother to restore it.

One other way would be to do this.picker.close = () => { }; after the date is changed, but you have to restore it always to be able to close the overlay.

You can get the selected date with the dateChange event of the matDatepickerInput and create an array with dates for multiple values and for having them selected in the view, you can use the dateClass input of the matDatepicker.

View:

<input [(ngModel)]="model" matInput [matDatepicker]="picker" placeholder="Choose a date" (dateChange)="dateChanged($event)" />
<mat-datepicker-toggle matPrefix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker [dateClass]="dateClass"></mat-datepicker>

Methods:

public dateClass = (date: Date) => {
    if (this.model.map((m) => +m).indexOf(+date) !== -1) {
        return [ 'selected' ];
    }
    return [ ];
}

public dateChanged(event: MatDatepickerInputEvent<Date>) {
    if (event.value) {
        const date = event.value;
        const index = this.model.map((m) => +m).indexOf(+date);
        if (index === -1) {
            this.model.push(date);
        } else {
            this.model.splice(index, 1)
        }
    }
}
Tim
  • 152
  • 10
0

in the case of the date range picker I solved this problem like below:

export class MatDatetimeComponent {
  @ViewChild(MatDateRangePicker) containerElRef;

  constructor(
    private resolver: ComponentFactoryResolver,
    private cdr: ChangeDetectorRef,
    private cdkConnectedOverlay: OverlayOutsideClickDispatcher
  ) {}

  // store prototype close object
  private selfClose: () => void;

  ngAfterViewInit() { 
    this.selfClose = this.containerElRef.close;
  }
  public onOpen() { 
    // rewrite autoclose after date is chosen
    this.containerElRef.close = () => {};

    // close calendar manually on outside click
    this.cdkConnectedOverlay._attachedOverlays[0]._outsidePointerEvents.subscribe(() => {
        // restore saved close method
        this.containerElRef.close = this.selfClose;
        this.containerElRef.close();
    });
  }
}
0

Because the other Solutions did not work for me, I did this:

Typescript Code:

  @ViewChild(MatDatepicker) datePicker: MatDatepicker<Date>;

  constructor(
    private changeDetectorRef: ChangeDetectorRef,
    private cdkConnectedOverlay: OverlayOutsideClickDispatcher) { }

  // store prototype close object
  private selfClose: () => void;

  opened() {
    if (isNullOrUndefined(this.selfClose)) {
      this.selfClose = this.datePicker.close;
    }

    // rewrite autoclose after date is chosen
    this.datePicker.close = () => {};

    // close calendar manually on outside click
    this.cdkConnectedOverlay._attachedOverlays[0]._outsidePointerEvents.subscribe(() => {
      // restore saved close method
      this.datePicker.close = this.selfClose;
      this.selfClose = undefined;
      this.datePicker.close();
    });
  }

Schema code:

  <mat-datepicker #picker
                  (opened)="opened()"
  ></mat-datepicker>

Maybe this can help someone out there.

Julian Egner
  • 221
  • 3
  • 8