Angular Material 18+ 高级教程 – Datepicker の Calendar & Custom DateAdapter (Temporal)
前言
本篇只会教 Angular Material Datepicker 里最关键的组件 -- Calendar 组件。
还有如何自定义 DateAdapter,让 Calendar 支持 TC39 Temporal。
有兴趣想学完整 Datepicker 的人,请移步官网。
只对 Calendar 组件和自定义 DateAdapter 感兴趣的,留下!
Let's start 。
Angular Material DateAdapter
App 组件
@Component({
selector: 'app-root',
standalone: true,
imports: [MatDatepickerModule], // 导入 Datepicker 模块
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
providers: [],
})
export class AppComponent {}
App Template
<mat-calendar [style.width.px]="256" />
效果
直接报错了,因为我们缺少了 DateAdapter。
Date Library
在讲解 DateAdapter 之前,我们先来聊聊 JavaScript 的 Date 和 Date Library。
JavaScript 使用 Date 来管理日期。
但凡用过 Date 的人都能轻易感觉到它的缺陷。
这个 Date 是 1995 年仿照 Java 设计出来的。
后来 Java 在 1997 年 1.1 版本和 2014 年 8.0 版本里,对 Date 进行了大量的改进。
而 JavaScript 却一直固步自封,把 1995 年的 Date 沿用到今天...
这就导致了,在真实项目中,Date 根本不够用,我们只能借助第三方 Date Library 来实现需求。
目前比较火的 Date Library 有:
四个 Library 都很受欢迎。
Why need DateAdapter?
对于开发者来说,有多个 Date Library 选择是一件好事,但对于 framework / library 而言就不好了。
像 Angular Material Datepicker 就需要同时支持所有受欢迎的 Date Library,否则就会出现一堆繁琐的转换操作。(e.g. convert Date to Luxon / Moment / Dayjs / date-fns date object)
相同的功能 (interface),却有不同的实现 (different library)。这...这不就是传说中面向对象 23 种设计模式之一的适配器模式吗?
Angular Material built-in DateAdapter
Angular Material 替我们做好了不同 Date Library 的 DateAdapter:
没想到最火的竟然是废弃了的 moment-adapter...
除了这 3 个 Date Library Adapter,还有一个是 JavaScript 原生 Date 的 Adapter -- NativeDateAdapter,总共 4 个 DateAdapter 可选。
NativeDateAdapter
回到 App 组件,添加一个 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: './app.component.html',
styleUrl: './app.component.scss',
providers: [provideNativeDateAdapter()], // 添加 NativeDateAdapter
})
export class AppComponent {}
注:你要放在 app.config.ts 作为全局 provider 也可以。
效果
没有再报错了
我们再加一个 selected date 给它,看看效果。
export class AppComponent {
readonly selectedDate = signal(new Date('2024-09-14')); // 因为是 NativeDateAdapter,所以这里使用 Date 对象
}
App Template
<mat-calendar [(selected)]="selectedDate" [style.width.px]="256" />
效果
13 号有 border 是因为 13 号是 today (本篇写于 2024-09-13),14 号有 background-color 是因为 14 号是 selected date。
LuxonDateAdapter
我们继续试试另一个 built-in 的 DateAdapter -- LuxonDateAdapter。
安装 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()], // 添加 LuxonDateAdapter
})
export class AppComponent {}
selected date 从 Date 对象改成 DateTime 对象 (Luxon 用的是 DateTime 对象,哎哟,有点像 .NET 哦)
import { DateTime } from 'luxon'; export class AppComponent {
// 因为是 LuxonDateAdapter,所以这里使用 DateTime 对象
readonly selectedDate = signal(DateTime.fromFormat('2024-09-14', 'yyyy-MM-dd'));
}
效果
和 NativeDateAdapter 出来的效果一模一样。
Angular Material custom DateAdapter for Temporal
Temporal 是 JavaScript 未来要取代 Date 的日期方案。它目前还在 Stage 3 阶段,没有任何游览器支持,但已经有完整的 Polyfill 可以使用了。
对他一窍不通的,请先看这篇 JavaScript – Temporal API & Date。
Angular 没有 built-in 的 Temporal DateAdapter,假如我们的项目使用 Temporal,那就需要自己搞一个 DateAdapter。
Let's start 。
Install Temporal
安装 Temporal Polyfill
yarn add temporal-polyfill
Create class TemporalDateAdapter
创建 class TemporalDateAdapter 并继承 abstract class DateAdapter
import { DateAdapter } from '@angular/material/core'; import { Temporal } from 'temporal-polyfill'; export class TemporalDateAdapter extends DateAdapter<Temporal.PlainDate> {}
DateAdapter 需要一个泛型,我们传入 Temporal.PlainDate,它仅仅代表日期 (年月日),不包含时间 (时分秒),也不包含时区 (time zone)。
这个 abstract class DateAdapter 里面有很多抽象方法,我们需要一一去实现。
源码在 date-adapter.ts
我们可以参考 LuxonDateAdapter (源码 luxon-date-adapter.ts) 和 NativeDateAdapter (源码 native-date-adapter.ts) 去实现上面这些 abstract methods。
Implement all abstract methods
我们一个一个来实现吧 。
constructor & locale
constructor() {
super();
this.locale = inject(MAT_DATE_LOCALE) as string;
}
locale 负责格式和语言,比如说是要显示 “January" 还是 "Jan" 还是 "一月"。
MAT_DATE_LOCALE 默认会拿 Angular 的 LOCALE_ID (这个我没有教过,以后教 i18n 时会补上),Angular LOCALE_ID 的默认值是 "en-US"。
总之,默认是美国英语就对了,想显示中文,我们可以把 LOCALE_ID 改成 "zh-Hans-CN" (zh-Hans 代表简体,CN 代表中国)。
app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
provideAnimations(),
{
provide: LOCALE_ID, // 设置整个项目的 locale
useValue: 'zh-Hans-CN',
},
{
provide: MAT_DATE_LOCALE, // 只设置 Datepicker 的 locale
useValue: 'zh-Hans-CN',
},
],
};
注:假如 MAT_DATE_LOCALE 和 LOCALE_ID 一样的话,只需要设置 LOCALE_ID 就可以了。
getYear
override getYear(date: Temporal.PlainDate): number {
return date.year;
}
太简单的,我就不解释了,下一个。
getMonth
override getMonth(date: Temporal.PlainDate): number {
return date.month - 1;
}
Temporal 一月是用 1 来表示,JavaScript Date 一月是用 0 来表示。
Angular Material 选择依据 JS Date 的标准来使用,所以我们需要额外做一个 -1 的动作。
getDate
override getDate(date: Temporal.PlainDate): number {
return date.day;
}
getDayOfWeek
override getDayOfWeek(date: Temporal.PlainDate): number {
return date.dayOfWeek === 7 ? 0 : date.dayOfWeek;
}
Temporal 星期日是用 7 来表示,JavaScript Date 星期日是用 0 来表示。
Angular Material 两个都兼容,返回 7 或者返回 0 都可以用来表示星期日。
getMonthNames
getMonthNames 就是要返回一个 string array,从一月到十二月,e.g. ['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 = Temporal.PlainDate.from({ year: 2017, month, day: 1 }); const formatter = new Intl.DateTimeFormat(this.locale, {
month: style,
});
return formatter.format(date); // long: January, short: Jan, narrow: J
});
}
我们借助 Intl.DateTimeFormat 处理 locale 就可以了。
getDateNames
getDateNames 就是要返回一个 string array,从 1 到 31,e.g. [1, 2, 3, ...days]。
override getDateNames(): string[] {
return new Array(31).fill(undefined).map<string>((_, index) => {
const day = index + 1;
const date = Temporal.PlainDate.from({ year: 2017, month: 1, day }); const formatter = new Intl.DateTimeFormat(this.locale, {
day: 'numeric',
});
// e.g. zh-Hans-CN: 31日
return formatter.format(date);
});
}
有一个小知识点:Intl.DateTimeFormat 中文会以 '日' 这个字作为结尾。换作是 MomentDateAdapter 的话,它用的是 Moment.format('D'),不会出现 '日' 这个字。
这写微差就看项目的需求咯,要怎样 format 都是我们自定义的。
getDayOfWeekNames
getDayOfWeekNames 就是要返回一个 string array,从星期日到星期六,e.g. ['Sun', 'Mon', 'Tue', ...dayOfWeeks]。
override getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
return new Array(7).fill(undefined).map<string>((_, index) => {
const day = index + 1;
// 刚巧 2017-01-01 是星期日,dayOfWeek 也从星期日开始
const date = Temporal.PlainDate.from({ year: 2017, month: 1, day }); const formatter = new Intl.DateTimeFormat(this.locale, {
weekday: style,
});
return formatter.format(date); // long: Sunday, short: Sun, narrow: S
});
}
我们借助 Intl.DateTimeFormat 处理 locale 就可以了。
getYearName
override getYearName(date: Temporal.PlainDate): string {
const formatter = new Intl.DateTimeFormat(this.locale, {
year: 'numeric',
});
return formatter.format(date); // e.g. 2017 (注:zh-Hans-CN 也是 2017 而不是 2017年 哦)
}
getFirstDayOfWeek
override getFirstDayOfWeek(): number {
return 0;
}
由于上面 getDayOfWeek 是以 0 来表示星期日,所以这里也 follow。
一周的第一天是星期日,所以返回 0。
当然,这个是美国的习惯。马来西亚,中国通常第一天是星期一,那我们可以改成 return 1。
getNumDaysInMonth
返回特定月份里有多少天,比如 28天,29天,30天,或者 31天。
override getNumDaysInMonth(date: Temporal.PlainDate): number {
return date.daysInMonth;
}
clone
克隆一个日期
override clone(date: Temporal.PlainDate): Temporal.PlainDate {
return date.with({});
}
createDate
创建日期对象对象
override createDate(year: number, month: number, date: number): Temporal.PlainDate {
return Temporal.PlainDate.from({ year, month: month + 1, day: date });
}
today
返回今天
override today(): Temporal.PlainDate {
return Temporal.Now.plainDateISO();
}
addCalendarYears, addCalendarMonths, addCalendarDays
添加年月日
override addCalendarYears(date: Temporal.PlainDate, years: number): Temporal.PlainDate {
return date.add({ years });
}
override addCalendarMonths(date: Temporal.PlainDate, months: number): Temporal.PlainDate {
return date.add({ months });
}
override addCalendarDays(date: Temporal.PlainDate, days: number): Temporal.PlainDate {
return date.add({ days });
}
toIso8601
返回 ISO 8601 日期格式
override toIso8601(date: Temporal.PlainDate): string {
return date.toString(); // 2017-01-01 year-month-day
}
isDateInstance
判断 value 是不是一个日期对象
override isDateInstance(obj: any): boolean {
return obj instanceof Temporal.PlainDate;
}
isValid
判断是不是一个 valid 的日期
override isValid(_date: Temporal.PlainDate): boolean {
return true;
}
JavaScript Date 有一个 Invalid Date 的概念
const date = new Date('whatever');
console.log(date.toString()); // Invalid Date
console.log(date.getTime()); // NaN
意思是 value 虽然是日期对象,但它是 invalid 的。
Temporal 没有这个概念,只要是 Temporal 对象,那它一定就是一个 valid 的日期。
所以 isValid 方法直接 return true 就可以了。
invalid
invalid 要返回一个 Invalid Date。
override invalid(): Temporal.PlainDate {
throw new Error('Method not implemented.');
}
Temporal 没有 Invalid Date 概念,这里我们直接 throw error 就可以了。
format
format 就是把 Temporal convert to date string with specify display format。
override format(date: Temporal.PlainDate, displayFormat: any): string {
const formatter = new Intl.DateTimeFormat(this.locale, {
...displayFormat,
timeZone: 'UTC',
});
return formatter.format(date);
}
参数 displayFormat 也是我们自定义的,透过 provider MAT_DATE_FORMATS,它的默认值是
不同的 DateAdapter 会搭配不同的 MatDateFormats,比如说 Luxon 的 displayFormat 是用 string 来表达的 e.g. yyyy-MM-dd = 2017-01-01 (完成的 format list 看这里)
parse
有 format 自然也有 parse。
override parse(value: any, _parseFormat: any): Temporal.PlainDate | null {
if (typeof value !== 'string') throw new Error('Method not implemented.');
const date = new Date(value);
if (!Number.isNaN(date.getTime())) {
return toTemporalInstant.call(date).toZonedDateTimeISO('UTC').toPlainDate();
}
return null;
}
同样的,参数 parseFormat 也是自定义的,来自 MatDateFormats.parse.dateInput (当 user 使用 input 输入一个 date string 时,date adapter 要把它从 string 转换成日期对象)。
我这里只是写了一个简单的 parse 作为例子,真实项目中 parse 通常会比较复杂。
deserialize
deserialize 和 parse 很类似,但是 deserialize 只用于 parse ISO 8601 string format。
而 parse 则可以处理 whatever string format 甚至是 whatever types.
override deserialize(value: any): Temporal.PlainDate | 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 super.deserialize(value);
}
上面这段,我是从 NativeDateAdapter 抄过来的。
总结
以上就是所有需要 implement 的 abstract methods。
parse, format, deserialize 需要依据不同项目需求去做调整,其它的基本上是通用的。
Try TemporalDateAdapter
App 组件
@Component({
selector: 'app-root',
standalone: true,
imports: [MatDatepickerModule, TestAdapterComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
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 {
// 因为是 TemporalDateAdapter,所以这里使用 Temporal.PlainDate 对象
readonly selectedDate = signal(Temporal.PlainDate.from('2024-09-14'));
}
注:provider 也可以放到全局 app.config.ts。
效果
Angular Material Calendar
我们先看看官网给出的第一个 Datepicker 例子
<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>
这里头涉及了 8 个组件 / 指令:
MatFormField 组件 -- <mat-form-field>
- MatLabel 指令 -- <mat-label>
MatInput 指令 -- input[matInput]
MatDatepickerInput 指令 -- input[matDatepicker]
- MatHint 指令 -- <mat-hint>
MatDatepickerToggle 组件 -- <mat-datepicker-toggle>
- MatSuffix 指令 -- [matIconSuffix]
MatDatepicker 组件 -- <mat-datepicker>
一上来就这么乱...我们把不相关的通通砍掉。
<input [matDatepicker]="picker">
<mat-datepicker-toggle [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
剩下三个:MatDatepickerInput 指令,MatDatepickerToggle 组件,MatDatepicker 组件。
效果
点击 MatDatepickerToggle 后会 popup MatDatepicker,选择日期后会 update 到 MatDatepickerInput。
toggle 和 input 只是配角,主角是 date picker。
而 MatDatepicker 的底层便是本篇要讲解的 Calendar 组件。
Calendar 组件
我们在上一 part Custom DateAdapter 里就已经使用 Calendar 组件作为例子了。
<mat-calendar [style.width.px]="300" />
效果
它有一些常见的 @Input / @Output 可以做配置。
selected
readonly selectedDate = signal(Temporal.PlainDate.from('2024-09-14'));
<pre>{{ selectedDate() }}</pre>
<mat-calendar [(selected)]="selectedDate" [style.width.px]="300" />
效果
Calendar 组件没有支持 Reactive Forms,它的 selected 就是简单的 @Input @Output 或者说 Two-way Binding。
minDate, maxDate, dateFilter
minDate, maxDate 用于限制 Calendar 可选范围。
比如,只能选今天前后七天的日期。
private readonly today = signal(Temporal.Now.plainDateISO());
readonly minDate = computed(() => this.today().subtract({ days: 7 }));
readonly maxDate = computed(() => this.today().add({ days: 7 }));
<mat-calendar [minDate]="minDate()" [maxDate]="maxDate()" [style.width.px]="300" />
效果
灰色的日期都是 disabled,不可选了。
dateFilter 则是依据日期做判断,是否可选。
readonly dateFilterFn = signal((date: Temporal.PlainDate) => date.dayOfWeek !== 6 && date.dayOfWeek !== 7); // 星期六和星期日以外才可选
<mat-calendar [dateFilter]="dateFilterFn()" [style.width.px]="300" />
效果
startAt, startView
startAt 是说,Calendar 初始画面要显示在哪一天?(默认是今天)
readonly startAt = Temporal.PlainDate.from('2017-01-01'); // 显示 2017年1月1日
<mat-calendar [startAt]="startAt" [style.width.px]="300" />
提醒:startAt 只对初始画面有效哦,后续修改 startAt 日期,Calendar 是不会同步更新的。
效果
startView 则是指初始画面要显示哪一种 view?
Calendar 一共有 3 种 view。
<mat-calendar startView="multi-year" [style.width.px]="300" />
提醒:和 startAt 一样,startView 只对初始画面有效,后续修改 startView 值,Calendar 是不会同步更新的。
效果
dateClass
我们可以给特定的日期添加 class,然后做 styling。
readonly dateClassFn = signal<MatCalendarCellClassFunction<Temporal.PlainDate>>((date, _view) => {
if (date.dayOfWeek === 7 || date.dayOfWeek === 6) return 'weekend';
return '';
});
给周末添加 ’weekend‘ class。
接着 styling
mat-calendar ::ng-deep .weekend {
background-color: pink;
}
需要使用 ::ng-deep 哦,不然渗透不进去。
效果
另外,dateClass 目前有一个 Bug -- Github Issue – bug(MatCalendar): updating dateClass does not update calendar view。
当我们修改 dateClass @input 值后,它不会立刻更新 Calendar。对比 dateFilter 则会立刻更新的。
selected DateRange
selected 可以选一个日期,也可以选一个 range。
比如说,从 2024-09-01 到 2024-09-30,选择一整个月份。
注:它只是可以选 range,而不是选 multiple 日期哦。
readonly selectedDateRange = signal(
new DateRange(Temporal.PlainDate.from('2024-09-01'), Temporal.PlainDate.from('2024-09-30')),
);
使用 DateRange 对象,参数 1 是 start date,参数 2 是 end date。
<p>start: {{ selectedDate().start }}</p>
<p>end: {{ selectedDate().end }}</p>
<mat-calendar [(selected)]="selectedDateRange" [style.width.px]="300" />
效果
selectedChange
上一个例子,假如我们去交互,它会坏掉。
因为 Calendar 的 two-way binding 不支持 date range。
我们需要动一点手脚。
首先,把 two-way binding 给拆开
<mat-calendar [selected]="selectedDateRange()" (selectedChange)="handleSelectedChange($event)" [style.width.px]="300" />
App 组件
private selectCount = 1; handleSelectedChange(selectedDate: Temporal.PlainDate | null) {
if (!selectedDate) return; // 分 2 次操作
// 第一次 select start
// 第二次 select end
// 然后循环
const selectCount = ((this.selectCount - 1) % 2) + 1;
this.selectCount++; const { selectedDateRange } = this; if (selectCount === 1) {
// select start
this.selectedDateRange.set(new DateRange(selectedDate, null));
return;
} if (selectCount === 2) {
const start = selectedDateRange().start!;
// 如果第二次 selectedDate 早于 start 就 re-select
if (Temporal.PlainDate.compare(selectedDate, start) === -1) {
selectedDateRange.set(new DateRange(selectedDate, null));
this.selectCount--;
return;
}
// select end
selectedDateRange.set(new DateRange(start, selectedDate));
return;
}
}
效果
MAT_DATE_RANGE_SELECTION_STRATEGY
MAT_DATE_RANGE_SELECTION_STRATEGY 可以增加交互体验。
App 组件 provider
@Component({
providers: [
{
provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
useClass: DefaultMatCalendarRangeStrategy,
},
],
})
export class AppComponent {}
效果
注意到那个 hover 后出现的 dash border 吗?它叫 preview。
comparisonStart,comparisonEnd
comparison 是对比的意思,通常用于对比 2 个 date range。
比如说,想拿上个星期 2024-09-01 到 2024-09-07 的数据对比这个星期 2024-09-08 到 2024-09-17 的数据。
readonly comparisonStart = signal(Temporal.PlainDate.from('2024-09-08'));
readonly comparisonEnd = signal(Temporal.PlainDate.from('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()"
[style.width.px]="300"
/>
注:comparison 分成 2 个 @Input 哦,而且它不是使用 DateRange 对象 (不统一啊)。
效果
2 个 date range 的颜色是不同的。
另外 selected date range 的头尾日期会有深色 background-color,但 comparison date range 没有 (不统一啊)。
styling
想修改样式就 override 它的 CSS variables。
@use '@angular/material' as mat; mat-calendar {
@include mat.datepicker-date-range-colors(hotpink, teal, yellow, purple); /* 等价于 */
--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;
}
效果
还有很多 variables 可以换,这里我就不一一列出了,用 Chrome DevTools 去找 element 然后看 styles 就可以了。
select comparisonStart and comparisonEnd
从上面两个 "不统一",我们可以猜得出来,comparison 这个功能一定是后来硬加进去 Calendar 的。
而事实也是如此,甚至连 select date range 功能也是后来才硬加进去的,所以 two-way binding 才不 work。
看到 Angular Material 团队有多糟糕了吧...
我们看看 Google 自家产品 Google Ads,如何 select comparison date range
接着看看 Angular Material 如何 select comparison date range
没错,它必须分开使用 2 个 Calendar 来做选择,而不像 Google Ads 那样只在一个 Calendar 上做选择。
看到吗?Angular Material 团队的无能对 Google 来说并不会有多大的影响,因为人家有能力自己实现。
但社区就不一定了,只能接受 Angular Material 团队的无能,使用体验较差的实现方式。
好,我们勉强来实现这个功能 -- 在同一个 Calendar 里选择 comparison date range。
App 组件
export class AppComponent {
readonly selectedDateRange = signal(new DateRange<Temporal.PlainDate>(null, null));
readonly comparisonDateRange = signal(new DateRange<Temporal.PlainDate>(null, null)); private readonly totalSelectCount = signal(1);
readonly selectCount = computed(() => ((this.totalSelectCount() - 1) % 4) + 1); handleSelectedChange(selectedDate: Temporal.PlainDate | null) {
if (!selectedDate) return; const { selectedDateRange, comparisonDateRange, totalSelectCount } = this;
const currSelectCount = this.selectCount();
totalSelectCount.set(totalSelectCount() + 1); // 分 4 次操作
// select start > end > comparison start > comparison end,然后一直循环
if (currSelectCount === 1) {
// select start
selectedDateRange.set(new DateRange(selectedDate, null));
return;
} if (currSelectCount === 2) {
const start = selectedDateRange().start!;
const isPrevDate = Temporal.PlainDate.compare(selectedDate, start) === -1;
if (isPrevDate) {
// re-select due to selected date is early than start
selectedDateRange.set(new DateRange(selectedDate, null));
totalSelectCount.set(totalSelectCount() - 1);
return;
}
// select end
selectedDateRange.set(new DateRange(start, selectedDate));
return;
} if (currSelectCount === 3) {
// 这里做了一个偷龙转风
// 因为 Calendar 只能监听 selected,不能监听 comparison 的选择
comparisonDateRange.set(new DateRange(selectedDateRange().start, selectedDateRange().end));
selectedDateRange.set(new DateRange(selectedDate, null));
return;
} if (currSelectCount === 4) {
const startDate = selectedDateRange().start!;
const isPrevDate = Temporal.PlainDate.compare(selectedDate, startDate) === -1;
if (isPrevDate) {
// re-select due to selected date is early than start
selectedDateRange.set(new DateRange(selectedDate, null));
totalSelectCount.set(totalSelectCount() - 1);
return;
}
// 最后再偷龙转风多一次,让它回到正确位置
selectedDateRange.set(new DateRange(comparisonDateRange().start, comparisonDateRange().end));
comparisonDateRange.set(new DateRange(startDate, selectedDate));
return;
}
}
}
最特别的地方就是第三次 select 的时候做了一个偷龙转风,不完美,但勉强可以用,真的没招了。
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"
[style.width.px]="300"
[class]="'select-count-' + selectCount()"
/>
最特别的地方是加入了一个 class:select-count-n
App Styles
由于我们上面做了偷龙转风,所以 styling 也需要配合,不然颜色会跑掉。
@use '@angular/material' as mat; $color-theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$blue-palette,
tertiary: mat.$orange-palette,
),
)
); mat-calendar {
--primary: #{mat.get-theme-color($color-theme, 'primary')};
--on-primary: #{mat.get-theme-color($color-theme, 'on-primary')};
--primary-light: #{mat.get-theme-color($color-theme, 'primary-container')};
--on-primary-light: #{mat.get-theme-color($color-theme, 'on-primary-container')};
--tertiary: #{mat.get-theme-color($color-theme, 'tertiary')};
--on-tertiary: #{mat.get-theme-color($color-theme, 'on-tertiary')};
--tertiary-light: #{mat.get-theme-color($color-theme, 'tertiary-container')};
--on-tertiary-light: #{mat.get-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);
}
}
}
看上去好像很多代码,但其实都是类似重复的,之所以没有封装是因为一个一个看,copy paste 会更好管理。
最终效果
总结
Calendar 就介绍到这里。顺便说一说上面提到的 MatDatepickerInput 指令,MatDatepickerToggle 组件,MatDatepicker 组件。
MatDatepicker 组件主要是封装了 Calendar 组件,并且让它变成一个 Popover,底层技术就是 CDK Overlay。
MatDatepickerToggle 组件就只是一个简单的 trigger,负责 popup MatDatepicker。
MatDatepickerInput 指令则是链接 Calendar 的 selected 日期,链接的时候会利用 DateAdapter 做 parse 和 format。
还有一个叫 MatDateRangeInput 组件 <mat-date-range-input> 负责处理 date range 的情况。
对这些上层组件 / 指令感兴趣的朋友可以去逛一逛源码。我就不奉陪了。
总结
本篇主要介绍了 Angular Material Datepicker 模块当中的 Calendar 组件,还有如何自定义 DateAdapter。
希望以后有时间能再写一篇介绍其它相关的组件 / 指令。掰掰
目录
上一篇 Angular Material 18+ 高级教程 – Material Form Field
下一篇 TODO
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding
Angular Material 18+ 高级教程 – Datepicker の Calendar & Custom DateAdapter (Temporal)的更多相关文章
- Angular Material 教程之布局篇
Angular Material 教程之布局篇 (一) : 布局简介https://segmentfault.com/a/1190000007215707 Angular Material 教程之布局 ...
- Angular Material TreeTable Component 使用教程
一. 安装 npm i ng-material-treetable --save npm i @angular/material @angular/cdk @angular/animations -- ...
- Angular Material design设计
官网: https://material.io/design/ https://meterial.io/components 优秀的Meterial design站点: http://material ...
- [转]VS Code 扩展 Angular 6 Snippets - TypeScript, Html, Angular Material, ngRx, RxJS & Flex Layout
本文转自:https://marketplace.visualstudio.com/items?itemName=Mikael.Angular-BeastCode VSCode Angular Typ ...
- 基于 Angular Material 的 Data Grid 设计实现
自 Extensions 组件库发布以来,Data Grid 成为了使用及咨询最多的组件.最开始 Data Grid 的设计非常简陋,经过一番重构,组件质量有了质的提升. Extensions 组件库 ...
- Siki_Unity_2-9_C#高级教程(未完)
Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...
- Angular Material Starter App
介绍 Material Design反映了Google基于Android 5.0 Lollipop操作系统的原生应用UI开发理念,而AngularJS还发起了一个Angular Material ...
- 安装 Angular Material UI
文档 调色板 安装 ng add @angular/material ? Choose a prebuilt theme name, or "custom" for a custo ...
- Pandas之:Pandas高级教程以铁达尼号真实数据为例
Pandas之:Pandas高级教程以铁达尼号真实数据为例 目录 简介 读写文件 DF的选择 选择列数据 选择行数据 同时选择行和列 使用plots作图 使用现有的列创建新的列 进行统计 DF重组 简 ...
- ios cocopods 安装使用及高级教程
CocoaPods简介 每种语言发展到一个阶段,就会出现相应的依赖管理工具,例如Java语言的Maven,nodejs的npm.随着iOS开发者的增多,业界也出现了为iOS程序提供依赖管理的工具,它的 ...
随机推荐
- BootstrapTable 行内编辑解决方案:bootstrap-table-editor
最近开发的一个业务平台,是一个低代码业务平台.其中用到的了bootstrap-table组件.但是bootstrap-table自身不带编辑功能. 通过搜索发现,网上大部分的解决方案都是使用x-edi ...
- 怎么用git命令将其他分支的提交记录提取到当前分支上
您可以使用 Git 命令 "cherry-pick" 将其他分支的提交记录提取到当前分支上.以下是使用 cherry-pick 命令的步骤:1. 切换到当前分支: `git che ...
- 【VMware VCF】VMware Cloud Foundation Part 01:概述。
VMware Cloud Foundation(简称 VCF)是 VMware 打造的一套用于 Software Defined Data Center(SDDC)软件定义数据中心的全栈云平台解决方案 ...
- Divide Interval 题解
背景 太逊了,调了三次才调出来,所以写篇题解寄念.LC好睿智 题意 给你两个数 \(a,b\),现在要从 \(a\) 跑到 \(b\),每次可以将当前的 \(a\) 拆分成 \(2^n\times m ...
- 题解:AT_xmascon21_b Bad Mood
AT_xmascon21_b Bad Mood 题意 给定你一个 \(n\times m\) 的矩形. 以一条对角线为基础上,制作一个无向图,该图的顶点对应于格子的共有 \((m+1) \times ...
- java实现二维码登录功能
本文采用Springboot工程进行开发,使用Google的zxing生成二维码,直接放代码: <?xml version="1.0" encoding="UTF- ...
- JAVA并发编程理论基础
注:本文章是对极客时间<java并发编程实战>学习归纳总结,更多知识点可到原文 java并发编程实战 进行学习.如果侵权,联系删除: 一.并发编程的BUG的源头 1.1 缓存导致的可见性问 ...
- 【Mybatis + Spring】 Mybatis - Spring 结合
环境搭建 EvBuild 软件环境准备 - MySQL 5.0 + - IDEA 2018 + - JDK1.8 + 依赖包相关 - Junit单元测试 - JDBC驱动 - Mybatis 组件 - ...
- 【SpringBoot】Re 02 Import与自定义装配实现
Import的注册形式: 1.使用@Import导入一个或者多个类字节对象 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME ...
- 【Docker】03 基础操作
[Docker 本体操作相关] 检查Docker版本: docker -v 检查Docker当前状态: systemctl status docker 停止Docker与开启Docker system ...