7

I would like to catch the event that comes from the month "left" and "right" selection buttons, but I couldn't find any documentation about it.

material datepicker month selector image

What events are fired when the "left" and "right" month selection boxes are clicked in the Material Datepicker?

George Stocker
  • 57,289
  • 29
  • 176
  • 237
Kiss Gábor
  • 83
  • 1
  • 4
  • Your question as originally written did not detail enough for someone to follow what was going on without seeing that external link. I've edited it and added the image into the question; in the future please keep this in mind when creating questions. – George Stocker Aug 15 '19 at 13:57
  • It looks like there is a generic event you can tie into that may also be triggered when those buttons are changed: MatDatepickerInputEvent. https://material.angular.io/components/datepicker/api – George Stocker Aug 15 '19 at 14:16

6 Answers6

9

Well, my another response is wrong, Let's go to take another aproach. Imagine we has a CustomTemplate Header. As we want that it's looks like the original DatePicker header we will copy the template of this that is in gitHub of angular, but we will change the functions in button next an prev, so it's like

<div class="mat-calendar-header">
  <div class="mat-calendar-controls">
    <button mat-button type="button" class="mat-calendar-period-button"
            (click)="currentPeriodClicked()" [attr.aria-label]="periodButtonLabel"
            cdkAriaLive="polite">
      {{periodButtonText}}
      <div class="mat-calendar-arrow"
           [class.mat-calendar-invert]="calendar.currentView != 'month'"></div>
    </button>

    <div class="mat-calendar-spacer"></div>

    <ng-content></ng-content>

                <!--see that we change previousClicked by customPrev-->
    <button mat-icon-button type="button" class="mat-calendar-previous-button"
            [disabled]="!previousEnabled()" (click)="customPrev()"
            [attr.aria-label]="prevButtonLabel">
    </button>

                <!--see that we change nextClicked by customNext-->
    <button mat-icon-button type="button" class="mat-calendar-next-button"
            [disabled]="!nextEnabled()" (click)="customNext()"
            [attr.aria-label]="nextButtonLabel">
    </button>
  </div>
</div>  

Now we defined our customHeader extends from MatCalendarHeader

export class ExampleHeader extends MatCalendarHeader<any> {

  /** Handles user clicks on the period label. */
  currentPeriodClicked(): void {
    this.calendar.currentView = this.calendar.currentView == 'month' ? 'multi-year' : 'month';
  }

  /** Handles user clicks on the previous button. */
  customPrev(): void {
    console.log(this.calendar.activeDate)
    this.previousClicked()
  }

  /** Handles user clicks on the next button. */
  customNext(): void {
    console.log(this.calendar.activeDate)
    this.nextClicked()
  }

Then , just only say that use this ExampleHeader

<mat-form-field>
  <mat-label>Custom calendar header</mat-label>
  <input matInput [matDatepicker]="picker">
  <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
  <mat-datepicker #picker [calendarHeaderComponent]="exampleHeader"></mat-datepicker>
</mat-form-field>

See in stackblitz

Tom Smykowski
  • 25,487
  • 54
  • 159
  • 236
Eliseo
  • 50,109
  • 4
  • 29
  • 67
3

There's a work-around too that is use the dateClass

@ViewChild('picker',{static:false}) calendar: MatDatepicker<any>;
  setClass() {
    return (date: any) => {
      if (date.getDate() == 1) this.changeMonth(
         {month:date.getMonth()+1,
          year:date.getFullYear()
          });
    };
  }

  changeMonth(date)
  {
    console.log(date)
  }

where you has

 <mat-form-field>
  <mat-label>Choose a date</mat-label>
  <input matInput [matDatepicker]="picker">
  <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
  <mat-datepicker #picker [dateClass]="setClass()"></mat-datepicker>
</mat-form-field>

See stackblitz

Well, nobody has interest in know when the month change if he has no good reason. The typical reason is call to an API that return an array with "specials" days and show in the calendar.

To change the aspect of this specials days we can think that we can use the own function dateClass but remember that we are looking for the days after the function is executed (*). So we need use Renderer2 to add the class.

If the data we received is some like (see that "day" is a string)

  [
    {day:'2020-02-06',data:'one'},
    {day:'2020-02-16',data:'two'},
    {day:'2020-02-22',data:'three'}
  ]

Our function changeMonth becomes like:

changeMonth(date) {
    this.dataService.getData(date.month, date.year).subscribe(res => {
      setTimeout(() => {
        let elements = document.querySelectorAll(".calendar");
        if (elements && elements.length) {
          const cells = elements[0].querySelectorAll(".mat-calendar-body-cell");
          cells.forEach(x => {
            const date = new Date(x.getAttribute("aria-label"));
            const dateTxt =
              date.getFullYear() +
              "-" +
              ("00" + (date.getMonth() + 1)).slice(-2) +
              "-" +
              ("00" + date.getDate()).slice(-2);

            if (res.find(x => x.day == dateTxt))
            {
              this.renderer.addClass(x, "special");
            }
          });
        }
      });
    });
  }

(*) we can also ask for the "special days" in ngOnInit and, then simple use the dateClass function

Eliseo
  • 50,109
  • 4
  • 29
  • 67
2

In your component.ts listen to click events that land on your mat-calendar next/previous buttons. One unique identifier for mat-calendar buttons could be ariaLabel.

  @HostListener('click', ['$event'])
  onClick(event: any) {
    // get the clicked element
    if (event.target.ariaLabel && event.target.ariaLabel.includes("Previous month")) {
      console.log('previous');
    } else if (event.target.ariaLabel && event.target.ariaLabel.includes("Next month")) {
      console.log('next');
    }
  }
Joosep Parts
  • 5,372
  • 2
  • 8
  • 33
1

the only way I've found is, (idea from onthecode) in open, check for querySelectorAll of the buttons. some like

<mat-form-field>
  <input matInput [matDatepicker]="picker" placeholder="Choose a date">
  <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
  <mat-datepicker #picker (opened)="openCalendar()">
  </mat-datepicker>
</mat-form-field>

  constructor(private renderer: Renderer2) { }
  openCalendar() {
    setTimeout(() => {
      const buttons = document.querySelectorAll
      ('.mat-calendar-previous-button,.mat-calendar-next-button')
      if (buttons) {
        Array.from(buttons).forEach(button => {
          this.renderer.listen(button, "click", (event) => {
            console.log('Arrow button clicked')
          });
        })
      }
    })
  }

see stackblitz

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Thank you! I've found this aswell, the problem is that I can't "retrieve" the month that I clicked into, only that I've clicked the button. – Kiss Gábor Aug 16 '19 at 07:20
  • @KissGábor, Just add another aproach to the problem. I hope this help you :) – Eliseo Aug 16 '19 at 11:36
0

You could also extend the MatCalendarHeader class and store the original nextClicked and previousClicked functions and change the behavior to call your code instead.

You could create a new component like so:

import { Component, OnInit } from '@angular/core';
import { MatCalendarHeader } from '@angular/material/datepicker';

@Component({
  selector: 'app-custom-calendar-header',
  templateUrl: './custom-calendar-header.component.html',
  styleUrls: ['./custom-calendar-header.component.scss']
})
export class CustomCalendarHeaderComponent<D> extends MatCalendarHeader<D> implements OnInit {

  private originalNextClicked: Function = MatCalendarHeader.prototype.nextClicked;

  ngOnInit(): void {
    const self = this;
    MatCalendarHeader.prototype.nextClicked = function() : void {
      self.myNextClicked();
      self.originalNextClicked();
    };
  }

  myNextClicked(): void {
    console.log('my nextClicked function is called');
  }
}

And in the view:

<mat-calendar-header></mat-calendar-header>

And do the same for previousClicked function.

-1

on the Template where:

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

add an event listener when the datepicker is open and add to the buttons the click event...

TS:

initEvents(): void {
setTimeout(() => {
  const prev = document.querySelector('.mat-calendar-previous-button');
  const next = document.querySelector('.mat-calendar-next-button');
  prev.addEventListener('click', () => { // Previous Button
    console.log('Prev');
  });
  next.addEventListener('click', () => { // Next Button
    console.log('Prev');
  });

}, 150);

}

Hope it works :)

without the small delay, it will display u a null reference, anyway with the small delay it should work well, in any way there's a delay when the user clicks on that datepicker by the animation so..

btw: it displays null because the event occurs before the element (our buttons) are in the DOM...

EDIT: one more thing am this solution may not the best because ur register events every time the datepicker is opened, but its the best and the fastest way which i found, an a better way of doing that is by creating a custom header... but its a lot of codes to write.., u can check it on official docs if u want.

Y_Moshe
  • 424
  • 1
  • 5
  • 11
  • the monthSelected event is in another part of the datepicker, when you click on the year, and then select another year, and a month – Kiss Gábor Aug 15 '19 at 14:04
  • sry for response later, am looks like i missed understand u, am after 1.5h searching for a solution, i found one but looks like it displayed an error, so here's my solution. look edited post. – Y_Moshe Aug 16 '19 at 01:13
  • Thank you! I've found this aswell, the problem is that I can't "retrieve" the month that I clicked into, only that I've clicked the button. – Kiss Gábor Aug 16 '19 at 07:25