Location>code7788 >text

Angular Material 18+ Advanced Tutorial - Datepicker の Calendar & Custom DateAdapter (Temporal)

Popularity:129 ℃/2024-09-19 11:38:49

preamble

This post will only teach the most critical component in Angular Material Datepicker - the Calendar component.

And how to customize the DateAdapter so that Calendar supports theTC39 Temporal

For those interested in learning the full Datepicker, please go toofficial website

Those interested only in the Calendar component and custom DateAdapter, stay!

Let's start 🚀。

 

Angular Material DateAdapter

App Components

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [MatDatepickerModule], // Importing the Datepicker Module
  templateUrl: './',
  styleUrl: './',
  providers: [],
})
export class AppComponent {}

 App Template

<mat-calendar []="256" />

effect

It reports an error straight away because we're missing the DateAdapter.

Date Library

Before we get into DateAdapter, let's talk about JavaScript's Date and Date Library.

JavaScript uses Date to manage dates.

Anyone who has ever used a Date can easily feel itsflaws

The Date was modeled after Java in 1995.

Java has since made a number of improvements to Date in versions 1.1 in 1997 and 8.0 in 2014.

JavaScript, on the other hand, has been stuck in a rut, following 1995's Date to the present day... 🙈

This leads to the fact that in real projects, Date is simply not enough, and we have to use a third-party Date Library to fulfill our needs.

Currently the hot Date Library has:

  1. (2020 Abandoned. Hottest year ever.)

  2.  (light weight)

  3. date-fns ()

  4. Luxon (successor)

All four Libraries are popular 😱.

Why need DateAdapter?

Having multiple Date Library choices is a good thing for developers, but not for frameworks / libraries.

Something like the Angular Material Datepicker would need to support all the popular Date Libraries at the same time, otherwise there would be a bunch of tedious conversions. (. convert Date to Luxon / Moment / Dayjs / date-fns date object)

Same function (interface), but different library. This... This is one of the 23 object-oriented design patterns known asadapter modeWhat do you think?

Angular Material built-in DateAdapter

Angular Material does a good job of making DateAdapters for different Date Libraries for us:

  1. moment-adapter

  2. (under development...)

  3. date-fns-adapter

  4. luxon-adapter

I didn't realize the hottest thing was the abandoned moment-adapter 🙈...

Besides these 3 Date Library Adapters, there is another one for JavaScript native Date -- NativeDateAdapter, total 4 DateAdapters are available.

NativeDateAdapter

Go back to the App component and add a provider

import { Component } from '@angular/core';

import { provideNativeDateAdapter } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [MatDatepickerModule],
  templateUrl: './',
  styleUrl: './',
  providers: [provideNativeDateAdapter()], // Add NativeDateAdapter
})
export class AppComponent {}

Note: You want to put in as a global provider as well.

effect

No more errors were reported 🙂 .

Let's add another selected date to it and see what happens.

export class AppComponent {
  readonly selectedDate = signal(new Date('2024-09-14')); // Since this is a NativeDateAdapter, the Date object is used here.
}

App Template

<mat-calendar [(selected)]="selectedDate" []="256" />

effect

13 has border because 13 is today (this was written on 2024-09-13), and 14 has background-color because 14 is the selected date.

LuxonDateAdapter

Let's move on to another built-in DateAdapter -- LuxonDateAdapter.

Installation of Luxon

yarn add luxon
yarn add @types/luxon --dev
yarn add @angular/material-luxon-adapter

provider

import { provideLuxonDateAdapter } from '@angular/material-luxon-adapter';

@Component({
  providers: [provideLuxonDateAdapter()], // Add LuxonDateAdapter
})
export class AppComponent {}

selected date is changed from a Date object to a DateTime object (Luxon uses a DateTime object, oops, kind of like .)

import { DateTime } from 'luxon';

export class AppComponent {
  // Since this is a LuxonDateAdapter, the DateTime object is used here.
  readonly selectedDate = signal(('2024-09-14', 'yyyy-MM-dd'));
}

effect

It comes out exactly the same as the NativeDateAdapter 🙂 .

 

Angular Material custom DateAdapter for Temporal

Temporal is JavaScript's future date scheme to replace Date. It's currently in Stage 3 and doesn't have a browser.be in favor ofBut there has been a completePolyfill It's ready to use.

For those who know nothing about him, read this firstJavaScript – Temporal API & Date

Angular doesn't have a built-in Temporal DateAdapter, so if we're using Temporal in our project, we'll need to make our own.

Let's start 🚀。

Install Temporal

Install Temporal Polyfill

yarn add temporal-polyfill

Create class TemporalDateAdapter

Create class TemporalDateAdapter and inherit from abstract class DateAdapter.

import { DateAdapter } from '@angular/material/core';

import { Temporal } from 'temporal-polyfill';

export class TemporalDateAdapter extends DateAdapter<> {}

DateAdapter requires a generic type, which we pass in , which represents only the date (year, month, day), but not the time (hours, minutes, seconds), nor the time zone.

This abstract class DateAdapter has a lot of abstract methods in it that we need to implement one by one.

The source code is in the

We can refer to LuxonDateAdapter (source code) and NativeDateAdapter (source code) to implement the above abstract methods.

Implement all abstract methods

Let's realize it one by one 🚀.

constructor & locale

constructor() {
  super();
  this.locale = inject(MAT_DATE_LOCALE) as string;
}

The locale is responsible for formatting and language, such as whether to display "January" or "Jan" or "January".

MAT_DATE_LOCALE by default takes Angular'sLOCALE_ID (I didn't teach this one, I'll fill in later when I teach i18n.) The default value for Angular LOCALE_ID is "en-US".

Anyway, the default is US English, and if you want to display Chinese, we can change LOCALE_ID to "zh-Hans-CN" (zh-Hans stands for Simplified Chinese, CN stands for Chinese).

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    provideAnimations(),
    {
      provide: LOCALE_ID, // Set the locale for the entire project
      useValue: 'zh-Hans-CN',
    },
    {
      provide: MAT_DATE_LOCALE, // Only set the locale of the Datepicker
      useValue: 'zh-Hans-CN',
    },
  ],
};

Note: If MAT_DATE_LOCALE and LOCALE_ID are the same, you only need to set LOCALE_ID.

getYear

override getYear(date: ): number {
  return ;
}

Too simple for me to explain, next.

getMonth

override getMonth(date: ): number {
  return  - 1;
}

Temporal January is represented by 1, and JavaScript Date January is represented by 0.

Angular Material chooses to use JS Date as its standard, so we need to do an additional -1 action.

getDate

override getDate(date: ): number {
  return ;
}

getDayOfWeek

override getDayOfWeek(date: ): number {
  return  === 7 ? 0 : ;
}

Temporal Sundays are represented by 7, JavaScript Date Sundays are represented by 0.

Angular Material is compatible with both, and returning 7 or 0 can be used to indicate Sunday.

getMonthNames

getMonthNames is to return a string array from January to December, . ['Jan', 'Feb', 'Mar', ... . months].

import { Intl, Temporal } from 'temporal-polyfill';

override getMonthNames(style: 'long' | 'short' | 'narrow'): string[] {
  return new Array(12).fill(undefined).map<string>((_, index) => {
    const month = index + 1;
    const date = ({ year: 2017, month, day: 1 });

    const formatter = new (this.locale, {
      month: style,
    });
    return (date); // long: January, short: Jan, narrow: J
  });
}

We do this with the help of handling locale.

getDateNames

getDateNames is to return a string array from 1 to 31, . [1, 2, 3, ... . days].

override getDateNames(): string[] {
  return new Array(31).fill(undefined).map<string>((_, index) => {
    const day = index + 1;
    const date = ({ year: 2017, month: 1, day });

    const formatter = new (this.locale, {
      day: 'numeric',
    });
    // . zh-Hans-CN: 31st
    return (date); 
  });
}

A little trivia: Chinese ends with the character 'date'. If it were MomentDateAdapter, it would use ('D'), and the word 'date' would not appear.

It depends on the needs of the project to write the nuances, how we want to format are customized.

getDayOfWeekNames

getDayOfWeekNames is to return a string array from Sunday to Saturday, . ['Sun', 'Mon', 'Tue', . .dayOfWeeks].

override getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
  return new Array(7).fill(undefined).map<string>((_, index) => {
    const day = index + 1;
    // It just so happens that 2017-01-01 is a Sunday, and dayOfWeek also starts on a Sunday
    const date = ({ year: 2017, month: 1, day });

    const formatter = new (this.locale, {
      weekday: style,
    });
    return (date); // long: Sunday, short: Sun, narrow: S
  });
}

We do this with the help of handling locale.

getYearName

override getYearName(date: ): string {
  const formatter = new (this.locale, {
    year: 'numeric',
  });
  return (date); // . 2017 (Note: zh-Hans-CN is also 2017 not 2017 Oh)
}

getFirstDayOfWeek

override getFirstDayOfWeek(): number {
  return 0; 
}

Since getDayOfWeek above uses 0 to represent Sunday, it follows here as well.

The first day of the week is Sunday, so 0 is returned.

Of course, this is a US custom. Malaysia, China usually the first day is Monday, then we can change it to return 1.

getNumDaysInMonth

Returns the number of days in a given month, such as 28, 29, 30, or 31.

override getNumDaysInMonth(date: ): number {
  return ;
}

clone

Clone a date

override clone(date: ):  {
  return date.with({});
}

createDate

Creating a Date Object Object

override createDate(year: number, month: number, date: number):  {
  return ({ year, month: month + 1, day: date });
}

today

Back to today

override today():  {
  return ();
}

addCalendarYears, addCalendarMonths, addCalendarDays

Add year, month and day

override addCalendarYears(date: , years: number):  {
  return ({ years });
}
override addCalendarMonths(date: , months: number):  {
  return ({ months });
}
override addCalendarDays(date: , days: number):  {
  return ({ days });
}

toIso8601

Return to ISO 8601 date format

override toIso8601(date: ): string {
  return (); // 2017-01-01 year-month-day
}

isDateInstance

Determine if value is a date object

override isDateInstance(obj: any): boolean {
  return obj instanceof ;
}

isValid

Determine if a date is valid

override isValid(_date: ): boolean {
  return true;
}

JavaScript Date has a concept of Invalid Date.

const date = new Date('whatever');
(()); // Invalid Date
(());  // NaN

It means that although value is a date object, it is invalid.

Temporal does not have this concept, as long as it is a Temporal object, then it must be a valid date.

So the isValid method just returns true.

invalid

invalid To return an Invalid Date.

override invalid():  {
  throw new Error('Method not implemented.');
}

Temporal doesn't have a concept of Invalid Date, so we can just throw error here.

format

format that is, put Temporal convert to date string with specify display format。

override format(date: , displayFormat: any): string {
  const formatter = new (this.locale, {
    ...displayFormat,
    timeZone: 'UTC',
  });
  return (date);
}

The parameter displayFormat is also customizable through the provider MAT_DATE_FORMATS, and its default value is

Different DateAdapter will work with different MatDateFormats, for example Luxon's displayFormat is expressed as string . yyyy-MM-dd = 2017-01-01 (see the list of completed formats)here are)

parse

There is format and there is parse.

override parse(value: any, _parseFormat: any):  | null {
  if (typeof value !== 'string') throw new Error('Method not implemented.');
  const date = new Date(value);
  if (!(())) {
    return (date).toZonedDateTimeISO('UTC').toPlainDate();
  }
  return null;
}

Similarly, the parameter parseFormat is customized from (when the user inputs a date string using input, the date adapter has to convert it from a string to a date object).

I've just written a simple parse as an example; parse is usually more complex in real projects.

deserialize

deserialize is very similar to parse, but deserialize is only used to parse ISO 8601 string format.

(indicates contrast) parse Then you can handle the whatever string format even whatever types.

override deserialize(value: any):  | null {
  if (typeof value === 'string') {
    if (value === '') return null;

    const ISO_8601_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))?)?$/;

    if (ISO_8601_REGEX.test(value)) {
      return this.parse(value, undefined);
    }
  }
  return (value);
}

I copied the above paragraph from NativeDateAdapter.

summarize

These are all the abstract methods that need to be implemented.

parse, format, deserialize need to be adjusted according to the needs of different projects, the other is basically common.

Try TemporalDateAdapter

App Components

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [MatDatepickerModule, TestAdapterComponent],
  templateUrl: './',
  styleUrl: './',
  providers: [
    {
      provide: MAT_DATE_LOCALE,
      useValue: 'zh-Hans-CN',
    },
    {
      provide: DateAdapter,
      useClass: TemporalDateAdapter,
    },
    {
      provide: MAT_DATE_FORMATS,
      useValue: MAT_NATIVE_DATE_FORMATS,
    },
  ],
})
export class AppComponent {
  // Because it's a TemporalDateAdapter, we're using the object
  readonly selectedDate = signal(('2024-09-14'));
}

Note: Providers can also be placed globally.

effect

 

Angular Material Calendar

Let's take a look.official websiteThe first Datepicker example given

<mat-form-field>
  <mat-label>Choose a date</mat-label>
  <input matInput [matDatepicker]="picker">
  <mat-hint>MM/DD/YYYY</mat-hint>
  <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
  <mat-datepicker #picker></mat-datepicker>
</mat-form-field>

There are 8 components/instructions involved:

  1. MatFormField component -- <mat-form-field>

  2. MatLabel command -- <mat-label>
  3. MatInput command -- input[matInput]

  4. MatDatepickerInput command -- input[matDatepicker]

  5. MatHint command -- <mat-hint>
  6. MatDatepickerToggle component -- <mat-datepicker-toggle>

  7. MatSuffix command -- [matIconSuffix]
  8. MatDatepicker component -- <mat-datepicker>

It was such a mess when it came up 🙁 ... Let's cut out the irrelevant ones through.

<input [matDatepicker]="picker">
<mat-datepicker-toggle [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>

Three remain: the MatDatepickerInput command, the MatDatepickerToggle component, and the MatDatepicker component.

effect

Clicking on the MatDatepickerToggle will popup the MatDatepicker and update it to the MatDatepickerInput when the date is selected.

toggle and input are just supporting characters, the main character is the date picker.

At the bottom of the MatDatepicker is the Calendar component, which we're going to talk about in this post.

Calendar Component

We already used the Calendar component as an example in the previous part Custom DateAdapter.

<mat-calendar []="300" />

effect

It has some common @Input / @Output that can be configured.

selected

readonly selectedDate = signal(('2024-09-14'));
<pre>{{ selectedDate() }}</pre>
<mat-calendar [(selected)]="selectedDate" []="300" />

effect

The Calendar component is not supportedReactive FormsIt's selected is simply @Input @Output orTwo-way Binding

minDate, maxDate, dateFilter

minDate, maxDate are used to limit the optional range of Calendar.

For example, only dates seven days before and after today can be selected.

private readonly today = signal(());
readonly minDate = computed(() => this.today().subtract({ days: 7 }));
readonly maxDate = computed(() => this.today().add({ days: 7 }));
<mat-calendar [minDate]="minDate()" [maxDate]="maxDate()" []="300" />

effect

Grayed out dates are disabled and unselectable.

dateFilter is a date-based filter that determines whether a date is optional or not.

readonly dateFilterFn = signal((date: ) =>  !== 6 &&  !== 7); // Only available outside of Saturday and Sunday
<mat-calendar [dateFilter]="dateFilterFn()" []="300" />

effect

startAt, startView

startAt is the date on which the initial Calendar screen will be displayed. (Default is today)

readonly startAt = ('2017-01-01'); // Displayed January 1, 2017
<mat-calendar [startAt]="startAt" []="300" />

Reminder: startAt is only valid for the initial screen, the Calendar will not be synchronized with subsequent changes to the startAt date.

effect

The startView is the view to be displayed on the initial screen.

Calendar has 3 views.

<mat-calendar startView="multi-year" []="300" />

Reminder: Like startAt, startView is only valid for the initial screen, Calendar will not be synchronized with subsequent changes to the startView value.

effect

dateClass

We can add class to a specific date and then do styling.

readonly dateClassFn = signal<MatCalendarCellClassFunction<>>((date, _view) => {
  if ( === 7 ||  === 6) return 'weekend';
  return '';
});

Add 'weekend' class for weekends.

Then styling.

mat-calendar ::ng-deep .weekend {
  background-color: pink;
}

Need to use ::ng-deep Oh, or penetration won't work.

effect

Also, dateClass currently has a bug -- theGithub Issue – bug(MatCalendar): updating dateClass does not update calendar view

When we change the dateClass @input value, it doesn't update the Calendar immediately, whereas dateFilter does.

selected DateRange

selected can select a date or a range.

For example, select an entire month from 2024-09-01 to 2024-09-30.

Note: It is only possible to select range, not multiple dates.

readonly selectedDateRange = signal(
  new DateRange(('2024-09-01'), ('2024-09-30')),
);

Using the DateRange object, parameter 1 is the start date and parameter 2 is the end date.

<p>start: {{ selectedDate().start }}</p>
<p>end: {{ selectedDate().end }}</p>
<mat-calendar [(selected)]="selectedDateRange" []="300" />

effect

selectedChange

In the last example, if we went to interact, it would break.

on account of Calendar (used form a nominal expression) two-way binding unsupported date range。

We need to move a little bit.

First, take apart the two-way binding.

<mat-calendar [selected]="selectedDateRange()" (selectedChange)="handleSelectedChange($event)" []="300" />

App Components

private selectCount = 1;

handleSelectedChange(selectedDate:  | null) {
  if (!selectedDate) return;

  // 2 operations
  // First select start
  // Second select end
  // and then loop
  const selectCount = ((this.selectCount - 1) % 2) + 1;
  this.selectCount++;

  const { selectedDateRange } = this;

  if (selectCount === 1) {
    // select start
    this.(new DateRange(selectedDate, null));
    return;
  }

  if (selectCount === 2) {
    const start = selectedDateRange().start!;
    // If the second selectedDate is earlier than start, re-select.
    if ((selectedDate, start) === -1) {
      (new DateRange(selectedDate, null));
      this.selectCount--;
      return;
    }
    // select end
    (new DateRange(start, selectedDate));
    return;
  }
}

effect

MAT_DATE_RANGE_SELECTION_STRATEGY

MAT_DATE_RANGE_SELECTION_STRATEGY can add to the interactive experience.

App component provider

@Component({
  providers: [
    {
      provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
      useClass: DefaultMatCalendarRangeStrategy,
    },
  ],
})
export class AppComponent {}

effect

Notice the dash border that appears after the hover? It's called preview.

comparisonStart,comparisonEnd

Comparison means contrast and is usually used to compare 2 date ranges.

For example, let's say you want to compare last week's data from 2024-09-01 to 2024-09-07 to this week's data from 2024-09-08 to 2024-09-17.

readonly comparisonStart = signal(('2024-09-08'));
readonly comparisonEnd = signal(('2024-09-14'));
<p>start: {{ selectedDateRange().start }}</p>
<p>end: {{ selectedDateRange().end }}</p>
<p>comparison start: {{ comparisonStart() }}</p>
<p>comparison end: {{ comparisonEnd() }}</p>

<mat-calendar 
  [selected]="selectedDateRange()" 
  (selectedChange)="handleSelectedChange($event)"
  [comparisonStart]="comparisonStart()" 
  [comparisonEnd]="comparisonEnd()" 
  []="300" 
/>

Note: the comparison is split into 2 @Inputs, and it's not using the DateRange object (inconsistent 🙄).

effect

The colors of the 2 date ranges are different.

Also the selected date range has a dark background-color for the first and last dates, but the comparison date range does not (inconsistent 🙄).

styling

If you want to modify a style, override its CSS variables.

@use '@angular/material' as mat;

mat-calendar {
  @include -date-range-colors(hotpink, teal, yellow, purple);

  /* equivalence*/
  --mat-datepicker-calendar-date-in-range-state-background-color: hotpink;
  --mat-datepicker-calendar-date-in-comparison-range-state-background-color: teal;
  --mat-datepicker-calendar-date-in-overlap-range-state-background-color: yellow;
  --mat-datepicker-calendar-date-in-overlap-range-selected-state-background-color: purple;
}

effect

There are many more variables you can change, so I won't list them all here, just use Chrome DevTools to find element and look at styles.

select comparisonStart and comparisonEnd

From the two "inconsistencies 🙄" above, we can guess that the COMPARISON feature must have been hardwired into Calendar later.

And it's true, even the select date range feature was added later, so two-way binding doesn't work.

See how bad the Angular Material team is... 😔

Let's look at Google's own product, Google Ads, and see how it selects comparison date range.

Let's take a look at how Angular Material selects comparison date range.

That's right, it must use 2 Calendars separately to make the selection, not just one Calendar like Google Ads.

See, the incompetence of the Angular Material team isn't going to matter much to Google, because they're capable of implementing it themselves.

The community, however, is not so sure and will have to accept the incompetence of the Angular Material team to use the less experienced implementation 😔.

Okay, we'll just barely make it work -- selecting the comparison date range in the same Calendar.

App Components

export class AppComponent {
  readonly selectedDateRange = signal(new DateRange<>(null, null));
  readonly comparisonDateRange = signal(new DateRange<>(null, null));

  private readonly totalSelectCount = signal(1);
  readonly selectCount = computed(() => ((this.totalSelectCount() - 1) % 4) + 1);

  handleSelectedChange(selectedDate:  | null) {
    if (!selectedDate) return;

    const { selectedDateRange, comparisonDateRange, totalSelectCount } = this;
    const currSelectCount = this.selectCount();
    (totalSelectCount() + 1);

    // 4 operations
    // select start > end > comparison start > comparison end, then keep looping
    if (currSelectCount === 1) {
      // select start
      (new DateRange(selectedDate, null));
      return;
    }

    if (currSelectCount === 2) {
      const start = selectedDateRange().start!;
      const isPrevDate = (selectedDate, start) === -1;
      if (isPrevDate) {
        // re-select due to selected date is early than start
        (new DateRange(selectedDate, null));
        (totalSelectCount() - 1);
        return;
      }
      // select end
      (new DateRange(start, selectedDate));
      return;
    }

    if (currSelectCount === 3) {
      // Here's a stolen dragon to turn the wind
      // Because Calendar can only listen for selected, it can't listen for comparison.
      (new DateRange(selectedDateRange().start, selectedDateRange().end));
      (new DateRange(selectedDate, null));
      return;
    }

    if (currSelectCount === 4) {
      const startDate = selectedDateRange().start!;
      const isPrevDate = (selectedDate, startDate) === -1;
      if (isPrevDate) {
        // re-select due to selected date is early than start
        (new DateRange(selectedDate, null));
        (totalSelectCount() - 1);
        return;
      }
      // And finally steal the dragon to turn the wind one more time to get it back to the right position
      (new DateRange(comparisonDateRange().start, comparisonDateRange().end));
      (new DateRange(startDate, selectedDate));
      return;
    }
  }
}

The most special thing was doing a Stealing Dragon Turning Wind on the third select, not perfect but barely usable, really out of tricks 😔.

App Template

<p>start: {{ selectedDateRange().start }}</p>
<p>end: {{ selectedDateRange().end }}</p>
<p>comparison start: {{ comparisonDateRange().start }}</p>
<p>comparison end: {{ comparisonDateRange().end }}</p>

<mat-calendar 
  [selected]="selectedDateRange()" 
  (selectedChange)="handleSelectedChange($event)"
  [comparisonStart]="comparisonDateRange().start" 
  [comparisonEnd]="comparisonDateRange().end" 
  []="300" 
  [class]="'select-count-' + selectCount()"
/>

The most unusual thing is the inclusion of a class: select-count-n

App Styles

Since we did steal the dragon to turn the wind above, the styling needs to match as well or the color will run away.

@use '@angular/material' as mat;

$color-theme: -theme(
  (
    color: (
      theme-type: light,
      primary: mat.$blue-palette,
      tertiary: mat.$orange-palette,
    ),
  )
);

mat-calendar {
  --primary: #{-theme-color($color-theme, 'primary')};
  --on-primary: #{-theme-color($color-theme, 'on-primary')};
  --primary-light: #{-theme-color($color-theme, 'primary-container')};
  --on-primary-light: #{-theme-color($color-theme, 'on-primary-container')};
  --tertiary: #{-theme-color($color-theme, 'tertiary')};
  --on-tertiary: #{-theme-color($color-theme, 'on-tertiary')};
  --tertiary-light: #{-theme-color($color-theme, 'tertiary-container')};
  --on-tertiary-light: #{-theme-color($color-theme, 'on-tertiary-container')};

  &.select-count-2 ::ng-deep {
    /* select start */
    --mat-datepicker-calendar-date-selected-state-background-color: var(--primary);
    --mat-datepicker-calendar-date-selected-state-text-color: var(--on-primary);

    /* select preview */
    --mat-datepicker-calendar-date-preview-state-outline-color: var(--primary);
  }

  &.select-count-3 ::ng-deep {
    /* select start & end */
    --mat-datepicker-calendar-date-selected-state-background-color: var(--primary);
    --mat-datepicker-calendar-date-selected-state-text-color: var(--on-primary);

    /* select range */
    --mat-datepicker-calendar-date-in-range-state-background-color: var(--primary-light);

    .mat-calendar-body-in-range {
      --mat-datepicker-calendar-date-text-color: var(--on-primary-light);
    }
  }

  &.select-count-4 ::ng-deep {
    /* select start & end */
    .mat-calendar-body-comparison-start .mat-calendar-body-cell-content,
    .mat-calendar-body-comparison-end .mat-calendar-body-cell-content {
      background-color: var(--primary) !important;

      --mat-datepicker-calendar-date-text-color: var(--on-primary);
    }

    /* select range */
    --mat-datepicker-calendar-date-in-comparison-range-state-background-color: var(--primary-light);

    .mat-calendar-body-in-comparison-range {
      --mat-datepicker-calendar-date-text-color: var(--on-primary-light);
    }

    /* comparison start */
    --mat-datepicker-calendar-date-selected-state-background-color: var(--tertiary);
    --mat-datepicker-calendar-date-selected-state-text-color: var(--on-tertiary);

    /* comparison range preview */
    --mat-datepicker-calendar-date-preview-state-outline-color: var(--tertiary);
  }

  &.select-count-1 ::ng-deep {
    /* select start & end */
    --mat-datepicker-calendar-date-selected-state-background-color: var(--primary);
    --mat-datepicker-calendar-date-selected-state-text-color: var(--on-primary);

    /* select range */
    --mat-datepicker-calendar-date-in-range-state-background-color: var(--primary-light);

    .mat-calendar-body-in-range {
      --mat-datepicker-calendar-date-text-color: var(--on-primary-light);
    }

    /* comparison start & end */
    .mat-calendar-body-comparison-start .mat-calendar-body-cell-content,
    .mat-calendar-body-comparison-end .mat-calendar-body-cell-content {
      background-color: var(--tertiary) !important;

      --mat-datepicker-calendar-date-text-color: var(--on-tertiary);
    }

    /* comparison range */
    --mat-datepicker-calendar-date-in-comparison-range-state-background-color: var(--tertiary-light);

    .mat-calendar-body-in-comparison-range {
      --mat-datepicker-calendar-date-text-color: var(--on-tertiary-light);
    }
  }
}

It looks like a lot of code, but it's all similarly duplicated, and the reason it's not encapsulated is because looking at it one by one, copy paste would be better managed.

final result

summarize

That's all for Calendar. By the way, I would like to mention the MatDatepickerInput command, the MatDatepickerToggle component, and the MatDatepicker component mentioned above.

The MatDatepicker component essentially wraps the Calendar component and makes it aPopoverThe underlying technology isCDK Overlay

The MatDatepickerToggle component is just a simple trigger responsible for popup MatDatepicker.

The MatDatepickerInput command links to the selected date of the Calendar, using the DateAdapter to do the parse and format.

There is also a component called MatDateRangeInput component <mat-date-range-input> which is responsible for handling the date range case.

For those interested in these upper level components / commands, you can take a stroll through the source code. I'll leave you to it 🤪.

 

summarize

This post introduces the Calendar component of the Angular Material Datepicker module and how to customize the DateAdapter.

I hope to write another post about other related components / commands sometime in the future. Bye bye 👋

 

 

 

catalogs

Previous article.Angular Material 18+ Advanced Tutorial - Material Form Field

Next TODO

To see the table of contents, go toAngular 18+ Advanced Tutorial - Table of Contents

Like it please click recommend 👍 and if you find the tutorial content to be out of context with the new version please comment to let me know. happy coding 😊 💻