From 39a44525a607ce794e9c1405372ea54ae29d51be Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 20 Jul 2021 21:58:50 +0200 Subject: [PATCH] feat(material-luxon-adapter): add luxon date adapter Adds a new package called `@angular/material-luxon-adapter` that provides a date adapter to be used together with Luxon dates. --- .github/CODEOWNERS | 3 +- .ng-dev/commit-message.ts | 1 + angular-tsconfig.json | 1 + package.json | 2 + rollup-globals.bzl | 2 + scripts/deploy/publish-build-artifacts.sh | 1 + src/components-examples/package.json | 3 +- src/components-examples/tsconfig.json | 3 +- src/dev-app/tsconfig.json | 1 + src/e2e-app/tsconfig.json | 1 + src/material-luxon-adapter/BUILD.bazel | 53 ++ src/material-luxon-adapter/adapter/index.ts | 31 ++ .../adapter/luxon-date-adapter.spec.ts | 485 ++++++++++++++++++ .../adapter/luxon-date-adapter.ts | 255 +++++++++ .../adapter/luxon-date-formats.ts | 22 + src/material-luxon-adapter/index.ts | 9 + src/material-luxon-adapter/package.json | 33 ++ src/material-luxon-adapter/public-api.ts | 9 + src/material-luxon-adapter/require-config.js | 7 + .../tsconfig-tests.json | 29 ++ src/material-luxon-adapter/tsconfig.json | 14 + src/material/datepicker/datepicker.md | 23 + src/material/package.json | 3 +- test/karma.conf.js | 1 + tools/gulp/gulpfile.ts | 2 + tools/gulp/packages.ts | 1 + tools/gulp/tasks/unit-test.ts | 1 + tools/release/changelog.ts | 1 + .../release-output/release-packages.ts | 1 + tools/system-config-tmpl.js | 2 + tsconfig.json | 1 + yarn.lock | 10 + 32 files changed, 1007 insertions(+), 4 deletions(-) create mode 100644 src/material-luxon-adapter/BUILD.bazel create mode 100644 src/material-luxon-adapter/adapter/index.ts create mode 100644 src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts create mode 100644 src/material-luxon-adapter/adapter/luxon-date-adapter.ts create mode 100644 src/material-luxon-adapter/adapter/luxon-date-formats.ts create mode 100644 src/material-luxon-adapter/index.ts create mode 100644 src/material-luxon-adapter/package.json create mode 100644 src/material-luxon-adapter/public-api.ts create mode 100644 src/material-luxon-adapter/require-config.js create mode 100644 src/material-luxon-adapter/tsconfig-tests.json create mode 100644 src/material-luxon-adapter/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 20aeb4a1e85f..7a75033655ed 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -88,8 +88,9 @@ /src/cdk/text-field/** @mmalerba /src/cdk/tree/** @jelbourn @andrewseguin -# Moment adapter package +# Date adapters /src/material-moment-adapter/** @mmalerba +/src/material-luxon-adapter/** @crisbeto # Material experimental package /src/material-experimental/* @jelbourn diff --git a/.ng-dev/commit-message.ts b/.ng-dev/commit-message.ts index 2e113a8239ea..dfd48b1b157f 100644 --- a/.ng-dev/commit-message.ts +++ b/.ng-dev/commit-message.ts @@ -72,6 +72,7 @@ export const commitMessage: CommitMessageConfig = { 'material-experimental/popover-edit', 'material-experimental/selection', 'material-moment-adapter', + 'material-luxon-adapter', 'material/autocomplete', 'material/badge', 'material/bottom-sheet', diff --git a/angular-tsconfig.json b/angular-tsconfig.json index 5bfc20431279..db36ab2ad935 100644 --- a/angular-tsconfig.json +++ b/angular-tsconfig.json @@ -37,6 +37,7 @@ "node_modules/@angular/material/**", "node_modules/@angular/material-experimental/**", "node_modules/@angular/material-moment-adapter/**", + "node_modules/@angular/material-luxon-adapter/**", "node_modules/@angular/youtube-player/**" ] } diff --git a/package.json b/package.json index f99fd4eec752..366e83c11526 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "@types/gulp": "4.0.8", "@types/inquirer": "^7.3.1", "@types/jasmine": "^3.6.0", + "@types/luxon": "^1.27.0", "@types/marked": "^2.0.0", "@types/merge2": "^0.3.30", "@types/minimist": "^1.2.0", @@ -200,6 +201,7 @@ "minimatch": "^3.0.4", "minimist": "^1.2.0", "moment": "^2.18.1", + "luxon": "^2.0.0", "node-fetch": "^2.6.0", "parse5": "^6.0.1", "postcss": "^8.2.1", diff --git a/rollup-globals.bzl b/rollup-globals.bzl index b2f071c72f9c..b8e54dc2ae11 100644 --- a/rollup-globals.bzl +++ b/rollup-globals.bzl @@ -34,6 +34,7 @@ ROLLUP_GLOBALS = { "@angular/material": "ng.material", "@angular/material-experimental": "ng.materialExperimental", "@angular/material-moment-adapter": "ng.materialMomentAdapter", + "@angular/material-luxon-adapter": "ng.materialLuxonAdapter", "@angular/youtube-player": "ng.youtubePlayer", # MDC Web @@ -78,6 +79,7 @@ ROLLUP_GLOBALS = { "moment": "moment", "moment/locale/fr": "moment.locale.fr", "moment/locale/ja": "moment.locale.ja", + "luxon": "luxon", "protractor": "protractor", "rxjs": "rxjs", "rxjs/operators": "rxjs.operators", diff --git a/scripts/deploy/publish-build-artifacts.sh b/scripts/deploy/publish-build-artifacts.sh index e055f7367601..a7ae78522726 100755 --- a/scripts/deploy/publish-build-artifacts.sh +++ b/scripts/deploy/publish-build-artifacts.sh @@ -22,6 +22,7 @@ PACKAGES=( material material-experimental material-moment-adapter + # material-luxon-adapter TODO(crisbeto): enable this once we have a builds repo google-maps youtube-player ) diff --git a/src/components-examples/package.json b/src/components-examples/package.json index 7dcf02307ff3..3f911c36eec5 100644 --- a/src/components-examples/package.json +++ b/src/components-examples/package.json @@ -25,7 +25,8 @@ "@angular/common": "0.0.0-NG", "@angular/material": "0.0.0-PLACEHOLDER", "@angular/material-experimental": "0.0.0-PLACEHOLDER", - "@angular/material-moment-adapter": "0.0.0-PLACEHOLDER" + "@angular/material-moment-adapter": "0.0.0-PLACEHOLDER", + "@angular/material-luxon-adapter": "0.0.0-PLACEHOLDER" }, "dependencies": { "tslib": "0.0.0-TSLIB" diff --git a/src/components-examples/tsconfig.json b/src/components-examples/tsconfig.json index 88e62f15c978..c8c393e3be79 100644 --- a/src/components-examples/tsconfig.json +++ b/src/components-examples/tsconfig.json @@ -11,7 +11,8 @@ "@angular/cdk-experimental/*": ["../cdk-experimental/*"], "@angular/material/*": ["../material/*"], "@angular/material-experimental/*": ["../material-experimental/*"], - "@angular/material-moment-adapter": ["../material-moment-adapter/public-api.ts"] + "@angular/material-moment-adapter": ["../material-moment-adapter/public-api.ts"], + "@angular/material-luxon-adapter": ["../material-luxon-adapter/public-api.ts"] } }, "include": ["./**/*.ts"] diff --git a/src/dev-app/tsconfig.json b/src/dev-app/tsconfig.json index 970d538c1003..37a5b959abbc 100644 --- a/src/dev-app/tsconfig.json +++ b/src/dev-app/tsconfig.json @@ -14,6 +14,7 @@ "@angular/cdk-experimental/*": ["../cdk-experimental/*"], "@angular/cdk-experimental": ["../cdk-experimental"], "@angular/material-moment-adapter": ["../material-moment-adapter/public-api.ts"], + "@angular/material-luxon-adapter": ["../material-luxon-adapter/public-api.ts"], "@angular/google-maps": ["../google-maps"], "@angular/components-examples": ["../components-examples"], "@angular/components-examples/*": ["../components-examples/*"], diff --git a/src/e2e-app/tsconfig.json b/src/e2e-app/tsconfig.json index 4cef3e72c68a..f8ab016c2312 100644 --- a/src/e2e-app/tsconfig.json +++ b/src/e2e-app/tsconfig.json @@ -12,6 +12,7 @@ "@angular/cdk-experimental/*": ["../cdk-experimental/*"], "@angular/cdk-experimental": ["../cdk-experimental/"], "@angular/material-moment-adapter": ["../material-moment-adapter/"], + "@angular/material-luxon-adapter": ["../material-luxon-adapter/"], "@angular/components-examples": ["../components-examples/"], "@angular/components-examples/*": ["../components-examples/*"] } diff --git a/src/material-luxon-adapter/BUILD.bazel b/src/material-luxon-adapter/BUILD.bazel new file mode 100644 index 000000000000..5117ec0be8c8 --- /dev/null +++ b/src/material-luxon-adapter/BUILD.bazel @@ -0,0 +1,53 @@ +load("//tools:defaults.bzl", "ng_module", "ng_package", "ng_test_library", "ng_web_test_suite") + +package(default_visibility = ["//visibility:public"]) + +ng_module( + name = "material-luxon-adapter", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//src:dev_mode_types", + "//src/material/core", + "@npm//@angular/core", + "@npm//@types/luxon", + "@npm//luxon", + ], +) + +ng_test_library( + name = "unit_test_sources", + srcs = glob( + ["**/*.spec.ts"], + exclude = ["**/*.e2e.spec.ts"], + ), + deps = [ + ":material-luxon-adapter", + "//src/material/core", + "@npm//@types/luxon", + "@npm//luxon", + ], +) + +ng_web_test_suite( + name = "unit_tests", + # We need to load Luxon statically since it is not a named AMD module and needs to + # be manually configured through "require.js" which is used by "karma_web_test_suite". + static_files = [ + "@npm//luxon", + ], + deps = [ + ":require-config.js", + ":unit_test_sources", + ], +) + +ng_package( + name = "npm_package", + srcs = ["package.json"], + entry_point = ":public-api.ts", + tags = ["release-package"], + deps = [":material-luxon-adapter"], +) diff --git a/src/material-luxon-adapter/adapter/index.ts b/src/material-luxon-adapter/adapter/index.ts new file mode 100644 index 000000000000..4cd60e06ddd2 --- /dev/null +++ b/src/material-luxon-adapter/adapter/index.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgModule} from '@angular/core'; +import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE} from '@angular/material/core'; +import {MAT_LUXON_DATE_ADAPTER_OPTIONS, LuxonDateAdapter} from './luxon-date-adapter'; +import {MAT_LUXON_DATE_FORMATS} from './luxon-date-formats'; + +export * from './luxon-date-adapter'; +export * from './luxon-date-formats'; + +@NgModule({ + providers: [{ + provide: DateAdapter, + useClass: LuxonDateAdapter, + deps: [MAT_DATE_LOCALE, MAT_LUXON_DATE_ADAPTER_OPTIONS] + }], +}) +export class LuxonDateModule {} + + +@NgModule({ + imports: [LuxonDateModule], + providers: [{provide: MAT_DATE_FORMATS, useValue: MAT_LUXON_DATE_FORMATS}], +}) +export class MatLuxonDateModule {} diff --git a/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts b/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts new file mode 100644 index 000000000000..93f200dcf668 --- /dev/null +++ b/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts @@ -0,0 +1,485 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {LOCALE_ID} from '@angular/core'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; +import {DateTime} from 'luxon'; +import {LuxonDateModule} from './index'; +import {MAT_LUXON_DATE_ADAPTER_OPTIONS} from './luxon-date-adapter'; + +const JAN = 1, FEB = 2, MAR = 3, DEC = 12; + +describe('LuxonDateAdapter', () => { + let adapter: DateAdapter; + + if (!isSupported()) { + it('should pass', () => expect(1).toBe(1)); + return; + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule] + }).compileComponents(); + + adapter = TestBed.inject(DateAdapter); + adapter.setLocale('en-US'); + })); + + it('should get year', () => { + expect(adapter.getYear(DateTime.local(2017, JAN, 1))).toBe(2017); + }); + + it('should get month', () => { + expect(adapter.getMonth(DateTime.local(2017, JAN, 1))).toBe(0); + }); + + it('should get date', () => { + expect(adapter.getDate(DateTime.local(2017, JAN, 1))).toBe(1); + }); + + it('should get day of week', () => { + expect(adapter.getDayOfWeek(DateTime.local(2017, JAN, 1))).toBe(7); + }); + + it('should get long month names', () => { + expect(adapter.getMonthNames('long')).toEqual([ + 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', + 'October', 'November', 'December' + ]); + }); + + it('should get short month names', () => { + expect(adapter.getMonthNames('short')).toEqual([ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]); + }); + + it('should get narrow month names', () => { + expect(adapter.getMonthNames('narrow')).toEqual([ + 'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D' + ]); + }); + + it('should get month names in a different locale', () => { + adapter.setLocale('da-DK'); + + expect(adapter.getMonthNames('long')).toEqual([ + 'januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', + 'august', 'september', 'oktober', 'november', 'december' + ]); + }); + + it('should get date names', () => { + expect(adapter.getDateNames()).toEqual([ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', + '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31' + ]); + }); + + it('should get date names in a different locale', () => { + adapter.setLocale('ja-JP'); + + expect(adapter.getDateNames()).toEqual([ + '1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日', '11日', '12日', + '13日', '14日', '15日', '16日', '17日', '18日', '19日', '20日', '21日', '22日', '23日', '24日', + '25日', '26日', '27日', '28日', '29日', '30日', '31日' + ]); + }); + + it('should get long day of week names', () => { + expect(adapter.getDayOfWeekNames('long')).toEqual([ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' + ]); + }); + + it('should get short day of week names', () => { + expect(adapter.getDayOfWeekNames('short')).toEqual([ + 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' + ]); + }); + + it('should get narrow day of week names', () => { + expect(adapter.getDayOfWeekNames('narrow')).toEqual([ + 'S', 'M', 'T', 'W', 'T', 'F', 'S' + ]); + }); + + it('should get day of week names in a different locale', () => { + adapter.setLocale('ja-JP'); + + expect(adapter.getDayOfWeekNames('long')).toEqual([ + '日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日' + ]); + }); + + it('should get year name', () => { + expect(adapter.getYearName(DateTime.local(2017, JAN, 1))).toBe('2017'); + }); + + it('should get year name in a different locale', () => { + adapter.setLocale('ja-JP'); + expect(adapter.getYearName(DateTime.local(2017, JAN, 1))).toBe('2017'); + }); + + it('should get first day of week', () => { + expect(adapter.getFirstDayOfWeek()).toBe(0); + }); + + it('should create Luxon date', () => { + expect(adapter.createDate(2017, JAN, 1) instanceof DateTime).toBe(true); + }); + + it('should not create Luxon date with month over/under-flow', () => { + expect(() => adapter.createDate(2017, 12, 1)).toThrow(); + expect(() => adapter.createDate(2017, -1, 1)).toThrow(); + }); + + it('should not create Luxon date with date over/under-flow', () => { + expect(() => adapter.createDate(2017, JAN, 32)).toThrow(); + expect(() => adapter.createDate(2017, JAN, 0)).toThrow(); + }); + + it("should get today's date", () => { + expect(adapter.sameDate(adapter.today(), DateTime.local())) + .toBe(true, "should be equal to today's date"); + }); + + it('should parse string according to given format', () => { + expect(adapter.parse('1/2/2017', 'L/d/yyyy')!.toISO()) + .toEqual(DateTime.local(2017, JAN, 2).toISO()); + expect(adapter.parse('1/2/2017', 'd/L/yyyy')!.toISO()) + .toEqual(DateTime.local(2017, FEB, 1).toISO()); + }); + + it('should parse string according to first matching format', () => { + expect(adapter.parse('1/2/2017', ['L/d/yyyy', 'yyyy/d/L'])!.toISO()) + .toEqual(DateTime.local(2017, JAN, 2).toISO()); + + expect(adapter.parse('1/2/2017', ['yyyy/d/L', 'L/d/yyyy'])!.toISO()) + .toEqual(DateTime.local(2017, JAN, 2).toISO()); + }); + + it('should throw if parse formats are an empty array', () => { + expect(() => adapter.parse('1/2/2017', [])).toThrowError('Formats array must not be empty.'); + }); + + it('should parse number', () => { + let timestamp = new Date().getTime(); + expect(adapter.parse(timestamp, 'LL/dd/yyyy')!.toISO()) + .toEqual(DateTime.fromMillis(timestamp).toISO()); + }); + + it('should parse Date', () => { + let date = new Date(2017, JAN, 1); + expect(adapter.parse(date, 'LL/dd/yyyy')!.toISO()).toEqual(DateTime.fromJSDate(date).toISO()); + }); + + it('should parse DateTime', () => { + let date = DateTime.local(2017, JAN, 1); + expect(adapter.parse(date, 'LL/dd/yyyy')!.toISO()).toEqual(date.toISO()); + }); + + it('should parse empty string as null', () => { + expect(adapter.parse('', 'LL/dd/yyyy')).toBeNull(); + }); + + it('should parse invalid value as invalid', () => { + let d = adapter.parse('hello', 'LL/dd/yyyy'); + expect(d).not.toBeNull(); + expect(adapter.isDateInstance(d)).toBe(true); + expect(adapter.isValid(d as DateTime)) + .toBe(false, 'Expected to parse as "invalid date" object'); + }); + + it('should format date according to given format', () => { + expect(adapter.format(DateTime.local(2017, JAN, 2), 'LL/dd/yyyy')).toEqual('01/02/2017'); + }); + + it('should format with a different locale', () => { + let date = adapter.format(DateTime.local(2017, JAN, 2), 'DD'); + + expect(stripDirectionalityCharacters(date)).toEqual('Jan 2, 2017'); + adapter.setLocale('da-DK'); + + date = adapter.format(DateTime.local(2017, JAN, 2), 'DD'); + expect(stripDirectionalityCharacters(date)).toEqual('2. jan. 2017'); + }); + + it('should throw when attempting to format invalid date', () => { + expect(() => adapter.format(DateTime.fromMillis(NaN), 'LL/dd/yyyy')) + .toThrowError(/LuxonDateAdapter: Cannot format invalid date\./); + }); + + it('should add years', () => { + expect(adapter.addCalendarYears(DateTime.local(2017, JAN, 1), 1).toISO()) + .toEqual(DateTime.local(2018, JAN, 1).toISO()); + expect(adapter.addCalendarYears(DateTime.local(2017, JAN, 1), -1).toISO()) + .toEqual(DateTime.local(2016, JAN, 1).toISO()); + }); + + it('should respect leap years when adding years', () => { + expect(adapter.addCalendarYears(DateTime.local(2016, FEB, 29), 1).toISO()) + .toEqual(DateTime.local(2017, FEB, 28).toISO()); + expect(adapter.addCalendarYears(DateTime.local(2016, FEB, 29), -1).toISO()) + .toEqual(DateTime.local(2015, FEB, 28).toISO()); + }); + + it('should add months', () => { + expect(adapter.addCalendarMonths(DateTime.local(2017, JAN, 1), 1).toISO()) + .toEqual(DateTime.local(2017, FEB, 1).toISO()); + expect(adapter.addCalendarMonths(DateTime.local(2017, JAN, 1), -1).toISO()) + .toEqual(DateTime.local(2016, DEC, 1).toISO()); + }); + + it('should respect month length differences when adding months', () => { + expect(adapter.addCalendarMonths(DateTime.local(2017, JAN, 31), 1).toISO()) + .toEqual(DateTime.local(2017, FEB, 28).toISO()); + expect(adapter.addCalendarMonths(DateTime.local(2017, MAR, 31), -1).toISO()) + .toEqual(DateTime.local(2017, FEB, 28).toISO()); + }); + + it('should add days', () => { + expect(adapter.addCalendarDays(DateTime.local(2017, JAN, 1), 1).toISO()) + .toEqual(DateTime.local(2017, JAN, 2).toISO()); + expect(adapter.addCalendarDays(DateTime.local(2017, JAN, 1), -1).toISO()) + .toEqual(DateTime.local(2016, DEC, 31).toISO()); + }); + + it('should clone', () => { + let date = DateTime.local(2017, JAN, 1); + let clone = adapter.clone(date); + + expect(clone).not.toBe(date); + expect(clone.toISO()).toEqual(date.toISO()); + }); + + it('should compare dates', () => { + expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2017, JAN, 2))) + .toBeLessThan(0); + + expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2017, FEB, 1))) + .toBeLessThan(0); + + expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2018, JAN, 1))) + .toBeLessThan(0); + + expect(adapter.compareDate(DateTime.local(2017, JAN, 1), DateTime.local(2017, JAN, 1))).toBe(0); + + expect(adapter.compareDate(DateTime.local(2018, JAN, 1), DateTime.local(2017, JAN, 1))) + .toBeGreaterThan(0); + + expect(adapter.compareDate(DateTime.local(2017, FEB, 1), DateTime.local(2017, JAN, 1))) + .toBeGreaterThan(0); + + expect(adapter.compareDate(DateTime.local(2017, JAN, 2), DateTime.local(2017, JAN, 1))) + .toBeGreaterThan(0); + }); + + it('should clamp date at lower bound', () => { + expect(adapter.clampDate( + DateTime.local(2017, JAN, 1), DateTime.local(2018, JAN, 1), DateTime.local(2019, JAN, 1))) + .toEqual(DateTime.local(2018, JAN, 1)); + }); + + it('should clamp date at upper bound', () => { + expect(adapter.clampDate( + DateTime.local(2020, JAN, 1), DateTime.local(2018, JAN, 1), DateTime.local(2019, JAN, 1))) + .toEqual(DateTime.local(2019, JAN, 1)); + }); + + it('should clamp date already within bounds', () => { + expect(adapter.clampDate( + DateTime.local(2018, FEB, 1), DateTime.local(2018, JAN, 1), DateTime.local(2019, JAN, 1))) + .toEqual(DateTime.local(2018, FEB, 1)); + }); + + it('should count today as a valid date instance', () => { + let d = DateTime.local(); + expect(adapter.isValid(d)).toBe(true); + expect(adapter.isDateInstance(d)).toBe(true); + }); + + it('should count an invalid date as an invalid date instance', () => { + let d = DateTime.fromMillis(NaN); + expect(adapter.isValid(d)).toBe(false); + expect(adapter.isDateInstance(d)).toBe(true); + }); + + it('should count a string as not a date instance', () => { + let d = '1/1/2017'; + expect(adapter.isDateInstance(d)).toBe(false); + }); + + it('should count a Date as not a date instance', () => { + let d = new Date(); + expect(adapter.isDateInstance(d)).toBe(false); + }); + + it('should create valid dates from valid ISO strings', () => { + assertValidDate(adapter, adapter.deserialize('1985-04-12T23:20:50.52Z'), true); + assertValidDate(adapter, adapter.deserialize('1996-12-19T16:39:57-08:00'), true); + assertValidDate(adapter, adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true); + assertValidDate(adapter, adapter.deserialize('1990-13-31T23:59:00Z'), false); + assertValidDate(adapter, adapter.deserialize('1/1/2017'), false); + expect(adapter.deserialize('')).toBeNull(); + expect(adapter.deserialize(null)).toBeNull(); + assertValidDate(adapter, adapter.deserialize(new Date()), true); + assertValidDate(adapter, adapter.deserialize(new Date(NaN)), false); + assertValidDate(adapter, adapter.deserialize(DateTime.local()), true); + assertValidDate(adapter, adapter.deserialize(DateTime.invalid('Not valid')), false); + }); + + it('returned dates should have correct locale', () => { + adapter.setLocale('ja-JP'); + expect(adapter.createDate(2017, JAN, 1).locale).toBe('ja-JP'); + expect(adapter.today().locale).toBe('ja-JP'); + expect(adapter.parse('1/1/2017', 'L/d/yyyy')!.locale).toBe('ja-JP'); + expect(adapter.addCalendarDays(DateTime.local(), 1).locale).toBe('ja-JP'); + expect(adapter.addCalendarMonths(DateTime.local(), 1).locale).toBe('ja-JP'); + expect(adapter.addCalendarYears(DateTime.local(), 1).locale).toBe('ja-JP'); + }); + + it('should not change locale of DateTime passed as param', () => { + const date = DateTime.local(); + const initialLocale = date.locale; + expect(initialLocale).toBeTruthy(); + adapter.setLocale('ja-JP'); + adapter.getYear(date); + adapter.getMonth(date); + adapter.getDate(date); + adapter.getDayOfWeek(date); + adapter.getYearName(date); + adapter.getNumDaysInMonth(date); + adapter.clone(date); + adapter.parse(date, 'LL/dd/yyyy'); + adapter.format(date, 'LL/dd/yyyy'); + adapter.addCalendarDays(date, 1); + adapter.addCalendarMonths(date, 1); + adapter.addCalendarYears(date, 1); + adapter.toIso8601(date); + adapter.isDateInstance(date); + adapter.isValid(date); + expect(date.locale).toBe(initialLocale); + }); + + it('should create invalid date', () => { + assertValidDate(adapter, adapter.invalid(), false); + }); +}); + +describe('LuxonDateAdapter with MAT_DATE_LOCALE override', () => { + let adapter: DateAdapter; + + if (!isSupported()) { + it('should pass', () => expect(1).toBe(1)); + return; + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [{provide: MAT_DATE_LOCALE, useValue: 'da-DK'}] + }).compileComponents(); + + adapter = TestBed.inject(DateAdapter); + })); + + it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => { + const date = adapter.format(DateTime.local(2017, JAN, 2), 'DD'); + expect(stripDirectionalityCharacters(date)).toEqual('2. jan. 2017'); + }); +}); + +describe('LuxonDateAdapter with LOCALE_ID override', () => { + let adapter: DateAdapter; + + if (!isSupported()) { + it('should pass', () => expect(1).toBe(1)); + return; + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [{provide: LOCALE_ID, useValue: 'fr-FR'}] + }).compileComponents(); + + adapter = TestBed.inject(DateAdapter); + })); + + it('should take the default locale id from the LOCALE_ID injection token', () => { + const date = adapter.format(DateTime.local(2017, JAN, 2), 'DD'); + + // Some browsers add extra invisible characters that we should strip before asserting. + expect(stripDirectionalityCharacters(date)).toEqual('2 janv. 2017'); + }); +}); + +describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () => { + let adapter: DateAdapter; + + if (!isSupported()) { + it('should pass', () => expect(1).toBe(1)); + return; + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [{ + provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, + useValue: {useUtc: true} + }] + }).compileComponents(); + + adapter = TestBed.inject(DateAdapter); + })); + + describe('use UTC', () => { + it('should create Luxon date in UTC', () => { + // Use 0 since createDate takes 0-indexed months. + expect(adapter.createDate(2017, 0, 5).toISO()) + .toBe(DateTime.utc(2017, JAN, 5).toISO()); + }); + + it('should create today in UTC', () => { + const today = adapter.today(); + expect(today.toISO()).toBe(today.toUTC().toISO()); + }); + + it('should parse dates to UTC', () => { + const date = adapter.parse('1/2/2017', 'LL/dd/yyyy')!; + expect(date.toISO()).toBe(date.toUTC().toISO()); + }); + + it('should return UTC date when deserializing', () => { + const date = adapter.deserialize('1985-04-12T23:20:50.52Z')!; + expect(date.toISO()).toBe(date.toUTC().toISO()); + }); + }); + +}); + + +function isSupported(): boolean { + // As of version 2.0.0 Luxon doesn't support any version of IE so we have to skip the tests there. + return typeof navigator !== 'undefined' && !(/(msie|trident|edge)/i.test(navigator.userAgent)); +} + +function stripDirectionalityCharacters(str: string) { + return str.replace(/[\u200e\u200f]/g, ''); +} + +function assertValidDate(adapter: DateAdapter, d: DateTime | null, valid: boolean) { + expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`); + expect(adapter.isValid(d!)).toBe(valid, + `Expected ${d} to be ${valid ? 'valid' : 'invalid'},` + + ` but was ${valid ? 'invalid' : 'valid'}`); +} diff --git a/src/material-luxon-adapter/adapter/luxon-date-adapter.ts b/src/material-luxon-adapter/adapter/luxon-date-adapter.ts new file mode 100644 index 000000000000..ff3dc70e3a45 --- /dev/null +++ b/src/material-luxon-adapter/adapter/luxon-date-adapter.ts @@ -0,0 +1,255 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Inject, Injectable, Optional, InjectionToken} from '@angular/core'; +import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; +import { + DateTime as LuxonDateTime, + Info as LuxonInfo, + DateTimeOptions as LuxonDateTimeOptions, +} from 'luxon'; + +/** Configurable options for {@see LuxonDateAdapter}. */ +export interface MatLuxonDateAdapterOptions { + /** + * Turns the use of utc dates on or off. + * Changing this will change how Angular Material components like DatePicker output dates. + * {@default false} + */ + useUtc: boolean; +} + +/** InjectionToken for LuxonDateAdapter to configure options. */ +export const MAT_LUXON_DATE_ADAPTER_OPTIONS = + new InjectionToken('MAT_LUXON_DATE_ADAPTER_OPTIONS', { + providedIn: 'root', + factory: MAT_LUXON_DATE_ADAPTER_OPTIONS_FACTORY + }); + + +/** @docs-private */ +export function MAT_LUXON_DATE_ADAPTER_OPTIONS_FACTORY(): MatLuxonDateAdapterOptions { + return { + useUtc: false + }; +} + +/** Creates an array and fills it with values. */ +function range(length: number, valueFunction: (index: number) => T): T[] { + const valuesArray = Array(length); + for (let i = 0; i < length; i++) { + valuesArray[i] = valueFunction(i); + } + return valuesArray; +} + +/** Adapts Luxon Dates for use with Angular Material. */ +@Injectable() +export class LuxonDateAdapter extends DateAdapter { + private _useUTC: boolean; + + constructor(@Optional() @Inject(MAT_DATE_LOCALE) dateLocale: string, + @Optional() @Inject(MAT_LUXON_DATE_ADAPTER_OPTIONS) + options?: MatLuxonDateAdapterOptions) { + + super(); + this._useUTC = !!options?.useUtc; + this.setLocale(dateLocale || LuxonDateTime.local().locale); + } + + getYear(date: LuxonDateTime): number { + return date.year; + } + + getMonth(date: LuxonDateTime): number { + // Luxon works with 1-indexed months whereas our code expects 0-indexed. + return date.month - 1; + } + + getDate(date: LuxonDateTime): number { + return date.day; + } + + getDayOfWeek(date: LuxonDateTime): number { + return date.weekday; + } + + getMonthNames(style: 'long' | 'short' | 'narrow'): string[] { + return LuxonInfo.months(style, {locale: this.locale}); + } + + getDateNames(): string[] { + // At the time of writing, Luxon doesn't offer similar + // functionality so we have to fall back to the Intl API. + const dtf = new Intl.DateTimeFormat(this.locale, {day: 'numeric', timeZone: 'utc'}); + + return range(31, i => { + // Format a UTC date in order to avoid DST issues. + const date = LuxonDateTime.utc(2017, 1, i + 1).toJSDate(); + + // Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted + // dates while other browsers do not. We remove them to make output consistent and + // because they interfere with date parsing. + return dtf.format(date).replace(/[\u200e\u200f]/g, ''); + }); + } + + getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] { + // Note that we shift the array once, because Luxon returns Monday as the + // first day of the week, whereas our logic assumes that it's Sunday. See: + // https://moment.github.io/luxon/api-docs/index.html#infoweekdays + const days = LuxonInfo.weekdays(style, {locale: this.locale}); + days.unshift(days.pop()!); + return days; + } + + getYearName(date: LuxonDateTime): string { + return date.toFormat('yyyy'); + } + + getFirstDayOfWeek(): number { + // To customize the first day of the week, and can extend this adapter and override this method. + return 0; + } + + getNumDaysInMonth(date: LuxonDateTime): number { + return date.daysInMonth; + } + + clone(date: LuxonDateTime): LuxonDateTime { + return LuxonDateTime.fromObject(date.toObject()); + } + + createDate(year: number, month: number, date: number): LuxonDateTime { + if (month < 0 || month > 11) { + throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`); + } + + if (date < 1) { + throw Error(`Invalid date "${date}". Date has to be greater than 0.`); + } + + // Luxon uses 1-indexed months so we need to add one to the month. + const result = this._useUTC ? LuxonDateTime.utc(year, month + 1, date) : + LuxonDateTime.local(year, month + 1, date); + + if (!this.isValid(result)) { + throw Error(`Invalid date "${date}". Reason: "${result.invalidReason}".`); + } + + return result.setLocale(this.locale); + } + + today(): LuxonDateTime { + return (this._useUTC ? LuxonDateTime.utc() : LuxonDateTime.local()).setLocale(this.locale); + } + + parse(value: any, parseFormat: string | string[]): LuxonDateTime | null { + const options: LuxonDateTimeOptions = this._getOptions(); + + if (typeof value == 'string' && value.length > 0) { + const iso8601Date = LuxonDateTime.fromISO(value, options); + + if (this.isValid(iso8601Date)) { + return iso8601Date; + } + + const formats = Array.isArray(parseFormat) ? parseFormat : [parseFormat]; + + if (!parseFormat.length) { + throw Error('Formats array must not be empty.'); + } + + for (const format of formats) { + const fromFormat = LuxonDateTime.fromFormat(value, format, options); + + if (this.isValid(fromFormat)) { + return fromFormat; + } + } + + return this.invalid(); + } else if (typeof value === 'number') { + return LuxonDateTime.fromMillis(value, options); + } else if (value instanceof Date) { + return LuxonDateTime.fromJSDate(value, options); + } else if (value instanceof LuxonDateTime) { + return LuxonDateTime.fromMillis(value.toMillis(), options); + } + + return null; + } + + format(date: LuxonDateTime, displayFormat: string): string { + if (!this.isValid(date)) { + throw Error('LuxonDateAdapter: Cannot format invalid date.'); + } + return date + .setLocale(this.locale) + .toFormat(displayFormat, {timeZone: this._useUTC ? 'utc' : undefined}); + } + + addCalendarYears(date: LuxonDateTime, years: number): LuxonDateTime { + return date.plus({years}).setLocale(this.locale); + } + + addCalendarMonths(date: LuxonDateTime, months: number): LuxonDateTime { + return date.plus({months}).setLocale(this.locale); + } + + addCalendarDays(date: LuxonDateTime, days: number): LuxonDateTime { + return date.plus({days}).setLocale(this.locale); + } + + toIso8601(date: LuxonDateTime): string { + return date.toISO(); + } + + /** + * Returns the given value if given a valid Luxon or null. Deserializes valid ISO 8601 strings + * (https://www.ietf.org/rfc/rfc3339.txt) and valid Date objects into valid DateTime and empty + * string into null. Returns an invalid date for all other values. + */ + override deserialize(value: any): LuxonDateTime | null { + const options = this._getOptions(); + let date: LuxonDateTime | undefined; + if (value instanceof Date) { + date = LuxonDateTime.fromJSDate(value, options); + } + if (typeof value === 'string') { + if (!value) { + return null; + } + date = LuxonDateTime.fromISO(value, options); + } + if (date && this.isValid(date)) { + return date; + } + return super.deserialize(value); + } + + isDateInstance(obj: any): boolean { + return obj instanceof LuxonDateTime; + } + + isValid(date: LuxonDateTime): boolean { + return date.isValid; + } + + invalid(): LuxonDateTime { + return LuxonDateTime.invalid('Invalid Luxon DateTime object.'); + } + + /** Gets the options that should be used when constructing a new `DateTime` object. */ + private _getOptions(): LuxonDateTimeOptions { + return { + zone: this._useUTC ? 'utc' : undefined, + locale: this.locale + }; + } +} diff --git a/src/material-luxon-adapter/adapter/luxon-date-formats.ts b/src/material-luxon-adapter/adapter/luxon-date-formats.ts new file mode 100644 index 000000000000..1a0fff9e05a3 --- /dev/null +++ b/src/material-luxon-adapter/adapter/luxon-date-formats.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {MatDateFormats} from '@angular/material/core'; + + +export const MAT_LUXON_DATE_FORMATS: MatDateFormats = { + parse: { + dateInput: 'D', + }, + display: { + dateInput: 'D', + monthYearLabel: 'LLL yyyy', + dateA11yLabel: 'DD', + monthYearA11yLabel: 'LLLL yyyy', + }, +}; diff --git a/src/material-luxon-adapter/index.ts b/src/material-luxon-adapter/index.ts new file mode 100644 index 000000000000..676ca90f1ffa --- /dev/null +++ b/src/material-luxon-adapter/index.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './public-api'; diff --git a/src/material-luxon-adapter/package.json b/src/material-luxon-adapter/package.json new file mode 100644 index 000000000000..50d9659a5738 --- /dev/null +++ b/src/material-luxon-adapter/package.json @@ -0,0 +1,33 @@ +{ + "name": "@angular/material-luxon-adapter", + "version": "0.0.0-PLACEHOLDER", + "description": "Angular Material Luxon Adapter", + "repository": { + "type": "git", + "url": "https://github.com/angular/components.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/components/issues" + }, + "homepage": "https://github.com/angular/components#readme", + "peerDependencies": { + "@angular/material": "0.0.0-PLACEHOLDER", + "@angular/core": "0.0.0-NG", + "luxon": "^2.0.0" + }, + "dependencies": { + "tslib": "0.0.0-TSLIB" + }, + "ng-update": { + "packageGroup": [ + "@angular/material", + "@angular/cdk", + "@angular/material-luxon-adapter" + ] + }, + "sideEffects": false, + "publishConfig": { + "registry":"https://wombat-dressing-room.appspot.com" + } +} diff --git a/src/material-luxon-adapter/public-api.ts b/src/material-luxon-adapter/public-api.ts new file mode 100644 index 000000000000..c9246d3a07b8 --- /dev/null +++ b/src/material-luxon-adapter/public-api.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './adapter/index'; diff --git a/src/material-luxon-adapter/require-config.js b/src/material-luxon-adapter/require-config.js new file mode 100644 index 000000000000..8f76220b4664 --- /dev/null +++ b/src/material-luxon-adapter/require-config.js @@ -0,0 +1,7 @@ +// Require.js is being used by the karma bazel rules and needs to be configured to properly +// load AMD modules which are not explicitly named in their output bundle. +require.config({ + paths: { + 'luxon': '/base/npm/node_modules/luxon/build/amd/luxon' + } +}); diff --git a/src/material-luxon-adapter/tsconfig-tests.json b/src/material-luxon-adapter/tsconfig-tests.json new file mode 100644 index 000000000000..6793503244d5 --- /dev/null +++ b/src/material-luxon-adapter/tsconfig-tests.json @@ -0,0 +1,29 @@ +{ + "extends": "../bazel-tsconfig-build.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "../../dist/packages/material-luxon-adapter", + "rootDir": ".", + "rootDirs": [ + ".", + "../../dist/packages/material-luxon-adapter" + ], + "importHelpers": false, + "module": "umd", + "target": "es5", + "types": ["jasmine"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "paths": { + "@angular/material/*": ["../../dist/packages/material/*"] + } + }, + "include": [ + "**/*.ts", + "**/*.spec.ts", + "../dev-mode-types.d.ts" + ], + "exclude": [ + "**/*.e2e.spec.ts" + ] +} diff --git a/src/material-luxon-adapter/tsconfig.json b/src/material-luxon-adapter/tsconfig.json new file mode 100644 index 000000000000..a8acc041c9ca --- /dev/null +++ b/src/material-luxon-adapter/tsconfig.json @@ -0,0 +1,14 @@ +// Configuration for IDEs only. +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "baseUrl": ".", + "paths": { + "@angular/cdk/*": ["../cdk/*"], + "@angular/material/*": ["../material/*"], + "@angular/material": ["../material/public-api.ts"] + } + }, + "include": ["./**/*.ts", "../dev-mode-types.d.ts"] +} diff --git a/src/material/datepicker/datepicker.md b/src/material/datepicker/datepicker.md index bfb0ccc341fb..4d0f6241efac 100644 --- a/src/material/datepicker/datepicker.md +++ b/src/material/datepicker/datepicker.md @@ -325,6 +325,29 @@ The easiest way to ensure this is to import one of the provided date modules: +`MatLuxonDateModule` (installed via `@angular/material-luxon-adapter`) + + + + + + + + + + + + + + + + + + + + +
Date typeDateTime
Supported localesSee project for details
DependenciesLuxon
Import from@angular/material-luxon-adapter
+ `MatMomentDateModule` (installed via `@angular/material-moment-adapter`) diff --git a/src/material/package.json b/src/material/package.json index 96816ca7b31c..595337a36498 100644 --- a/src/material/package.json +++ b/src/material/package.json @@ -34,7 +34,8 @@ "packageGroup": [ "@angular/material", "@angular/cdk", - "@angular/material-moment-adapter" + "@angular/material-moment-adapter", + "@angular/material-luxon-adapter" ] }, "sideEffects": false, diff --git a/test/karma.conf.js b/test/karma.conf.js index 08071ec86e35..e494f88108b7 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -44,6 +44,7 @@ module.exports = config => { included: false, watched: false }, + {pattern: 'node_modules/luxon/build/amd/**/*', included: false, watched: false}, {pattern: 'node_modules/@material/*/dist/*', included: false, watched: false}, {pattern: 'node_modules/kagekiri/**', included: false, watched: false}, diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index 37ceecdd7afa..f5b9b37b9036 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -6,6 +6,7 @@ import { materialExperimentalPackage, materialPackage, momentAdapterPackage, + luxonAdapterPackage, youTubePlayerPackage } from './packages'; @@ -15,6 +16,7 @@ createPackageBuildTasks(cdkExperimentalPackage); createPackageBuildTasks(materialPackage); createPackageBuildTasks(materialExperimentalPackage); createPackageBuildTasks(momentAdapterPackage); +createPackageBuildTasks(luxonAdapterPackage); createPackageBuildTasks(youTubePlayerPackage); createPackageBuildTasks(googleMapsPackage); diff --git a/tools/gulp/packages.ts b/tools/gulp/packages.ts index bb38623decdc..1573c9d4a58e 100644 --- a/tools/gulp/packages.ts +++ b/tools/gulp/packages.ts @@ -8,3 +8,4 @@ export const cdkExperimentalPackage = new BuildPackage('cdk-experimental', [cdkP export const materialExperimentalPackage = new BuildPackage('material-experimental', [cdkPackage, cdkExperimentalPackage, materialPackage]); export const momentAdapterPackage = new BuildPackage('material-moment-adapter', [materialPackage]); +export const luxonAdapterPackage = new BuildPackage('material-luxon-adapter', [materialPackage]); diff --git a/tools/gulp/tasks/unit-test.ts b/tools/gulp/tasks/unit-test.ts index 712903b8e277..9bded80c86a2 100644 --- a/tools/gulp/tasks/unit-test.ts +++ b/tools/gulp/tasks/unit-test.ts @@ -28,6 +28,7 @@ task(':test:build', series( 'material-experimental:build-no-bundles', 'youtube-player:build-no-bundles', 'material-moment-adapter:build-no-bundles', + 'material-luxon-adapter:build-no-bundles', 'google-maps:build-no-bundles', ':test:build-system-config' )); diff --git a/tools/release/changelog.ts b/tools/release/changelog.ts index 5120b3459c09..98653f07b2a0 100644 --- a/tools/release/changelog.ts +++ b/tools/release/changelog.ts @@ -32,6 +32,7 @@ const orderedChangelogPackages = [ 'google-maps', 'youtube-player', 'material-moment-adapter', + 'material-luxon-adapter', 'cdk-experimental', 'material-experimental', ]; diff --git a/tools/release/release-output/release-packages.ts b/tools/release/release-output/release-packages.ts index 0c8aa28898c2..43d2f65c6c9a 100644 --- a/tools/release/release-output/release-packages.ts +++ b/tools/release/release-output/release-packages.ts @@ -7,4 +7,5 @@ export const releasePackages = [ 'cdk-experimental', 'material-experimental', 'material-moment-adapter', + 'material-luxon-adapter', ]; diff --git a/tools/system-config-tmpl.js b/tools/system-config-tmpl.js index 01788aa15709..d1884d7f9ff3 100644 --- a/tools/system-config-tmpl.js +++ b/tools/system-config-tmpl.js @@ -31,6 +31,7 @@ var nodeModulesPath = '$NODE_MODULES_BASE_PATH'; var pathMapping = { 'tslib': 'node:tslib/tslib.js', 'moment': 'node:moment/min/moment-with-locales.min.js', + 'luxon': 'node:luxon/build/amd/luxon.js', 'moment/locale': 'node:moment/locale', 'kagekiri': 'node:kagekiri/dist/kagekiri.umd.min.js', @@ -153,6 +154,7 @@ function setupLocalReleasePackages() { configureEntryPoint('material'); configureEntryPoint('material-experimental'); configureEntryPoint('material-moment-adapter'); + configureEntryPoint('material-luxon-adapter'); configureEntryPoint('google-maps'); configureEntryPoint('youtube-player'); diff --git a/tsconfig.json b/tsconfig.json index 53b81013a601..06264dd9e5b6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ "@angular/cdk-experimental/*": ["./src/cdk-experimental/*"], "@angular/cdk-experimental": ["./src/cdk-experimental"], "@angular/material-moment-adapter": ["./src/material-moment-adapter"], + "@angular/material-luxon-adapter": ["./src/material-luxon-adapter"], "@angular/components-examples": ["./src/components-examples"], "@angular/components-examples/*": ["./src/components-examples/*"] } diff --git a/yarn.lock b/yarn.lock index 2f9d54cf4127..1c052c6b44a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2349,6 +2349,11 @@ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== +"@types/luxon@^1.27.0": + version "1.27.1" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.27.1.tgz#aceeb2d5be8fccf541237e184e37ecff5faa9096" + integrity sha512-cPiXpOvPFDr2edMnOXlz3UBDApwUfR+cpizvxCy0n3vp9bz/qe8BWzHPIEFcy+ogUOyjKuCISgyq77ELZPmkkg== + "@types/marked@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-2.0.0.tgz#9319de90410be0ba43d5cad0ede2c26e57edb9eb" @@ -8949,6 +8954,11 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +luxon@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.0.1.tgz#b41ca2f1f5ad8099c18603ae6c36a7794039daf0" + integrity sha512-8Eawf81c9ZlQj62W3eq4mp+C7SAIAnmaS7ZuEAiX503YMcn+0C1JnMQRtfaQj6B5qTZLgHv0F4H5WabBCvi1fw== + macos-release@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"