User interface
Angular Components Material SPA Typescript Visual Studio Code

Filtering Date Ranges with the Angular Material Date Picker

Welcome to today’s post.

In today’s post I will be discussing how to use date filters within an Angular Material Date Picker control.

In a previous post, I showed how to setup and use the Angular Material Date Picker Control.

Once you have setup the Material Date Picker control, you are ready to make use of the date range picker. The date range picker control allows the exclusion of individual dates or ranges of dates from input selection. This is useful when there are business rules that are required to be applied to the selection of dates instead of letting the user select a date then performing the validating after the date range picker has closed. I will explain how the date range picker control is used in the following sections.

Understanding the Material Date Range Control

The Material Date Picker has two main UI components that are used to define the input and output display:

  1. A date selector button that pops up a date picker to the user.
  2. A text control that displays the selected date.

Within the HTML template we declare three different controls with tags:

1. A <mat-date-range-input> control that allows us to specify the following:

a. A range picker that switches the user between the start and end dates when selecting the dates from the popup date picker. The popup picker control will be explained a little later.

b. A date range filter that allows us to restrict the selectable dates to the user within the date popup. I will show how to restrict this later.

An example of the HTML declaration is below:

<mat-date-range-input [rangePicker]="picker" [dateFilter]="rangeFilter">

2. A pair of <input> input controls with attributes matStartDate and matEndDate respectively.

An example of this input control declaration is shown below:

<input type="text" name="dateRangeStart" id="dateRangeStart"  
    matStartDate placeholder="Start date" 
    (dateInput)="dateRangeChanged($event)" 
    (dateChange)="dateRangeChanged($event)">

3. A date picker <mat-datepicker-toggle> control that specifies the date picker popup that appears when the user clicks on the date picker button. This is the picker control name that is assigned to the [rangePicker] attribute for the control <mat-date-range-input> above.

An example of the HTML declaration is below:

<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker>

On combining the above controls, we have the date range picker control below:

<mat-form-field>
<mat-label>Date Range</mat-label>
    <mat-date-range-input [rangePicker]="picker" [dateFilter]="rangeFilter">
        <input type="text" name="dateRangeStart" id="dateRangeStart"  
            matStartDate placeholder="Start date" 
            (dateInput)="dateRangeChanged($event)" 
            (dateChange)="dateRangeChanged($event)">
        <input type="text" name="dateRangeEnd" id="dateRangeEnd"  
            matEndDate placeholder="End date"
            (dateInput)="dateRangeChanged($event)" 
            (dateChange)="dateRangeChanged($event)">
    </mat-date-range-input>
    <mat-datepicker-toggle matSuffix [for]="picker">
    </mat-datepicker-toggle>
    <mat-date-range-picker #picker></mat-date-range-picker>
</mat-form-field>

In the next section, I will be showing how to restrict dates within the data picker with the range filter.

Filtering to Restrict Dates with the Range Filter

In our HTML declaration, the attribute [dateFilter] when specified, requires an event handler function to be declared. By default, it returns no result, and the default is to allow all dates to be selectable.

The function result below returns true, allowing ALL dates to be selectable:

rangeFilter(date: Date): boolean {
    return true; 
}

By applying some logic within the range filter function, the result determines if the date corresponding to the input date parameter will be enabled for selection or greyed out (disabled).

Next, things get tricky. I will go through two different cases of date range filtering:

  1. Filtering a continuous range of dates to be selectable.
  2. Excluding individual dates from selection.

Filtering a Continuous Range of Dates to be Selectable

Before we write logic to filter a range of dates, I will explain how the range filter function event handler works. For each date within the displayed month that is rendered within the date picker, the range filter function is called with the currently cycled date within the month. We then write logic to determine if the currently cycled date is selectable.

To filter a range of dates and include them as selectable within the date picker we will need to compare each date in the month with a condition. If the current date satisfies the condition, then we can return a value of true. Below we have an example of a filtering condition that allows dates up until thirty days past the current date to be selectable:

let includeDatesWithinNextThirtyDays: 
boolean = date.valueOf() < (currentDate.valueOf() + 30*60*60*1000*24);

Note that our comparison uses the mathematical calculation that a day consists of 60 seconds in a minute, 60 minutes in an hour, and 24 hours within a day. Within each second there are 1000 milliseconds.

A JavaScript Date object has a value that increases by 60*60*24*1000 ticks per day, so in our date calculations, we can add or subtract these ticks to compare the cycled date against the current date.

So, to restrict selections to the next thirty days past the current date we have the range filter function handler:

rangeFilter(date: Date): boolean {
    let currentDate: Date = new Date();
    let includeDatesWithinNextThirtyDays: boolean = 
        date.valueOf() < (currentDate.valueOf() + 30*60*60*1000*24);
    return (includeDatesWithinNextThirtyDays); 
}

When our application refreshes, the date picker will show the following restricted dates. The first screen shows a current date 25th September 2021, with the next five days selectable:

The next month shows the next 15 days selectable up to the 15th October:

The case of excluding individual dates has a practical application in so example, an application that allows the booking of meetings. In this case we want to exclude public holidays. The next section describes this.

Excluding Individual Dates from Selection

The scenario we are entertaining is a public holiday within the currently viewed month, and we wish to exclude it from selection.

In the case below where the current month is September, the following month where I am based has a Labor Day public holiday on the first Monday of October. This is a case of excluding one day and including this logic within other filters within the filter handler.

The code except to handle this is shown below:

rangeFilter(date: Date): boolean {
    let currentDate: Date = new Date();

    let octoberLabourDayHoliday = new Date(currentDate.getFullYear(), 9, 1);
    let currentCheckDate: Date = octoberLabourDayHoliday; 
    for (let index = 0; index <= 8; index++) {
        const isDayOfWeekAMonday: boolean = (currentCheckDate.getDay() == 1);
        if (isDayOfWeekAMonday)
            break;
        currentCheckDate = 
 		    new Date(currentCheckDate.valueOf() + 60*60*1000*24);
    }

    let excludeLaborDayHoliday: boolean = 
        !((date.getMonth() === 9) && 
          (date.getDate() === currentCheckDate.getDate()));
    return (includeDatesWithinNextThirtyDays && excludeLaborDayHoliday);
}

When our application refreshes, the date picker will show the following restricted dates in October. The screen shows Monday 4th October not selectable (greyed out), with the 14 days surrounding this date up to 15th October selectable:

With other public holidays, where the exclusion date is a definite day of a month, such as the New Year Day Holiday, Christmas Holidays and Australia Day Public Holiday the logic is simpler.

Below is a logic within the range filter to cover these cases:

rangeFilter(date: Date): boolean {
    let currentDate: Date = new Date();

    let excludeNewYearPublicHoliday: boolean = 
        !(date.getDate() === 1 && date.getMonth() === 0);
    let excludeXmasPublicHoliday: boolean = 
        !(date.getDate() === 25 && date.getMonth() === 11);
    let excludeBoxingDayPublicHoliday: boolean = 
        !(date.getDate() === 26 && date.getMonth() === 11);
    let excludeAustraliaDayPublicHoliday: boolean = 
        !(date.getDate() === 26 && date.getMonth() === 0);
    let includeDatesWithinNextThirtyDays: boolean = 
        date.valueOf() < (currentDate.valueOf() + 30*60*60*1000*24);

    return (includeDatesWithinNextThirtyDays &&  
          excludeLaborDayHoliday && excludeNewYearPublicHoliday && 
          excludeXmasPublicHoliday && excludeBoxingDayPublicHoliday &&
          excludeAustraliaDayPublicHoliday); 
}

When our application refreshes, the date picker in December will show the following restricted dates as not selectable:

To be able to view the above restrictions in the future (remember our current date is in September), we need to simulate the current date as 1st December. We do this by replacing the current date variable as shown:

let currentDate: Date = new Date(2021, 11, 1);

Recall that the JavaScript Date object has zero-based months: 0…11.

When we advance to the next month, January, the date picker will show the following restricted dates as not selectable:

Again, like we did for December, to be able to view the above restrictions in the future (remember our current date is in September), we need to simulate the current date as 1st January. We do this by replacing the current date variable as shown:

let currentDate: Date = new Date(2022, 0, 1);

Notice that the 31st of January is excluded since we have kept the rule including any days 30 days past the current date.

With all the rules above, our range filter is as shown:

rangeFilter(date: Date): boolean {
    let currentDate: Date = new Date();

    let octoberLabourDayHoliday = new Date(currentDate.getFullYear(), 9, 1);
    let currentCheckDate: Date = octoberLabourDayHoliday; 
    for (let index = 0; index <= 8; index++) {
      const isDayOfWeekAMonday: boolean = (currentCheckDate.getDay() == 1);
      if (isDayOfWeekAMonday)
        break;
      currentCheckDate = new Date(currentCheckDate.valueOf() + 60*60*1000*24);
    }

    let excludeLaborDayHoliday: boolean = !((date.getMonth() === 9) && (date.getDate() === currentCheckDate.getDate()));
    let includeDatesAfterYesterday: boolean = (date.valueOf() > currentDate.valueOf() - 60*60*1000*24);
    let excludeNewYearPublicHoliday: boolean = !(date.getDate() === 1 && date.getMonth() === 0);
    let excludeXmasPublicHoliday: boolean = !(date.getDate() === 25 && date.getMonth() === 11);
    let excludeBoxingDayPublicHoliday: boolean = !(date.getDate() === 26 && date.getMonth() === 11);
    let excludeAustraliaDayPublicHoliday: boolean = !(date.getDate() === 26 && date.getMonth() === 0);
    let includeDatesWithinNextThirtyDays: boolean = date.valueOf() < (currentDate.valueOf() + 30*60*60*1000*24);

    return (includeDatesWithinNextThirtyDays && includeDatesAfterYesterday && 
          excludeLaborDayHoliday && excludeNewYearPublicHoliday && 
          excludeXmasPublicHoliday && excludeBoxingDayPublicHoliday &&
          excludeAustraliaDayPublicHoliday); 
}

So, to summarize, the conceptual logic we return from our filter function has the form:

[inclusion condition for range 1] && … [inclusion condition for range N] && 
[exclusion condition single date 1] && … [exclusion condition single date N]

Initially, using the range filter handler requires an understanding that the handler is called for each date within the visible selectable date range. Once this is understood, and how the current date and future dates can be tested, then we can apply logic to restrict to both date ranges and individual dates.

Applying the above logic within the filters will allow us to restrict the date ranges as needed, including allowing individual dates and date ranges to be excluded from selection within the date picker dialog.

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial