From 16958e244cb8b5a1d427c7e4781e474024660339 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 13 Jan 2019 18:17:47 +0100 Subject: [PATCH] feat: add date adapter for luxon Adds the `LuxonDateAdapter` that can be used to work with Luxon dates in the `MatDatepicker`. --- .github/CODEOWNERS | 3 + package.json | 2 + packages.bzl | 1 + scripts/deploy/publish-build-artifacts.sh | 4 +- src/dev-app/system-config.ts | 2 + src/dev-app/tsconfig-aot.json | 1 + src/dev-app/tsconfig-build.json | 1 + src/dev-app/tsconfig.json | 1 + src/e2e-app/tsconfig.json | 1 + src/lib/datepicker/datepicker.md | 15 +- src/lib/package.json | 3 +- src/material-examples/package.json | 3 +- src/material-examples/tsconfig-build.json | 1 + src/material-examples/tsconfig.json | 3 +- src/material-luxon-adapter/BUILD.bazel | 50 ++ src/material-luxon-adapter/adapter/index.ts | 33 ++ .../adapter/luxon-date-adapter.spec.ts | 498 ++++++++++++++++++ .../adapter/luxon-date-adapter.ts | 246 +++++++++ .../adapter/luxon-date-formats.ts | 23 + 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-build.json | 32 ++ .../tsconfig-tests.json | 28 + src/material-luxon-adapter/tsconfig.json | 14 + test/karma-system-config.js | 1 + test/karma.conf.js | 1 + tools/gulp/gulpfile.ts | 4 +- tools/gulp/packages.ts | 5 +- tools/gulp/tasks/aot.ts | 1 + tools/gulp/tasks/development.ts | 9 + tools/gulp/tasks/unit-test.ts | 3 +- tools/package-tools/rollup-globals.ts | 2 + .../release-output/release-packages.ts | 3 +- yarn.lock | 10 + 36 files changed, 1046 insertions(+), 16 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-build.json 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 e8674c26601d..10299de563af 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -82,6 +82,9 @@ # Moment adapter package /src/material-moment-adapter/** @mmalerba +# Luxon adapter package +/src/material-luxon-adapter/** @mmalerba @crisbeto + # Material experimental package /src/material-experimental/** @jelbourn diff --git a/package.json b/package.json index 08f818a70297..bd40813a31ba 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@types/hammerjs": "^2.0.35", "@types/inquirer": "^0.0.43", "@types/jasmine": "^3.0.0", + "@types/luxon": "^1.4.1", "@types/marked": "^0.4.2", "@types/merge2": "^0.3.30", "@types/minimist": "^1.2.0", @@ -114,6 +115,7 @@ "karma-parallel": "^0.3.0", "karma-sauce-launcher": "^2.0.2", "karma-sourcemap-loader": "^0.3.7", + "luxon": "^1.8.3", "magic-string": "^0.22.4", "marked": "^0.5.1", "merge2": "^1.2.3", diff --git a/packages.bzl b/packages.bzl index 59829ef3907c..70914ce69c27 100644 --- a/packages.bzl +++ b/packages.bzl @@ -90,6 +90,7 @@ VERSION_PLACEHOLDER_REPLACEMENTS = { ROLLUP_GLOBALS = { 'tslib': 'tslib', 'moment': 'moment', + 'luxon': 'luxon', '@angular/cdk': 'ng.cdk', '@angular/cdk-experimental': 'ng.cdkExperimental', '@angular/material': 'ng.material', diff --git a/scripts/deploy/publish-build-artifacts.sh b/scripts/deploy/publish-build-artifacts.sh index 29d3f4565a2e..b7689c78931c 100755 --- a/scripts/deploy/publish-build-artifacts.sh +++ b/scripts/deploy/publish-build-artifacts.sh @@ -16,7 +16,9 @@ if [ -z ${MATERIAL2_BUILDS_TOKEN} ]; then fi # Material packages that need to published. -PACKAGES=(cdk material material-moment-adapter) +PACKAGES=(cdk material material-moment-adapter material-luxon-adapter) + +## TODO(crisbeto): add luxon to this once we have a repo for `material-luxon-adapter` builds. REPOSITORIES=(cdk-builds material2-builds material2-moment-adapter-builds) # Command line arguments. diff --git a/src/dev-app/system-config.ts b/src/dev-app/system-config.ts index f1637d28e5d9..d1c180816bd9 100644 --- a/src/dev-app/system-config.ts +++ b/src/dev-app/system-config.ts @@ -18,6 +18,7 @@ System.config({ 'main': 'main.js', 'tslib': 'node:tslib/tslib.js', 'moment': 'node:moment/min/moment-with-locales.min.js', + 'luxon': 'node:luxon/build/amd/luxon.js', 'rxjs': 'node_modules/rxjs/bundles/rxjs.umd.min.js', 'rxjs/operators': 'system-rxjs-operators.js', @@ -43,6 +44,7 @@ System.config({ '@angular/material-experimental': 'dist/packages/material-experimental/index.js', '@angular/material-examples': 'dist/packages/material-examples/index.js', '@angular/material-moment-adapter': 'dist/packages/material-moment-adapter/index.js', + '@angular/material-luxon-adapter': 'dist/packages/material-luxon-adapter/index.js', '@angular/cdk': 'dist/packages/cdk/index.js', '@angular/cdk-experimental': 'dist/packages/cdk-experimental/index.js', diff --git a/src/dev-app/tsconfig-aot.json b/src/dev-app/tsconfig-aot.json index a7c7fc3451e2..cb971b17a3a9 100644 --- a/src/dev-app/tsconfig-aot.json +++ b/src/dev-app/tsconfig-aot.json @@ -29,6 +29,7 @@ "@angular/cdk-experimental/*": ["../../dist/releases/cdk-experimental/*"], "@angular/cdk-experimental": ["../../dist/releases/cdk-experimental"], "@angular/material-moment-adapter": ["../../dist/releases/material-moment-adapter"], + "@angular/material-luxon-adapter": ["../../dist/releases/material-luxon-adapter"], "@angular/material-examples": ["../../dist/releases/material-examples"] } }, diff --git a/src/dev-app/tsconfig-build.json b/src/dev-app/tsconfig-build.json index 8965db786ec9..35f53dd37012 100644 --- a/src/dev-app/tsconfig-build.json +++ b/src/dev-app/tsconfig-build.json @@ -36,6 +36,7 @@ "@angular/cdk-experimental/*": ["../../dist/packages/cdk-experimental/*"], "@angular/cdk-experimental": ["../../dist/packages/cdk-experimental"], "@angular/material-moment-adapter": ["../../dist/packages/material-moment-adapter"], + "@angular/material-luxon-adapter": ["../../dist/packages/material-luxon-adapter"], "@angular/material-examples": ["../../dist/packages/material-examples"] } }, diff --git a/src/dev-app/tsconfig.json b/src/dev-app/tsconfig.json index d09e8f865c06..342a32f85a5c 100644 --- a/src/dev-app/tsconfig.json +++ b/src/dev-app/tsconfig.json @@ -15,6 +15,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/material-examples": ["../../dist/packages/material-examples"] } }, diff --git a/src/e2e-app/tsconfig.json b/src/e2e-app/tsconfig.json index 4fd572d6eb50..2bf431185058 100644 --- a/src/e2e-app/tsconfig.json +++ b/src/e2e-app/tsconfig.json @@ -13,6 +13,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/material-examples": ["../material-examples/"] } }, diff --git a/src/lib/datepicker/datepicker.md b/src/lib/datepicker/datepicker.md index ce5c6672e19f..d8ec0754a001 100644 --- a/src/lib/datepicker/datepicker.md +++ b/src/lib/datepicker/datepicker.md @@ -212,16 +212,15 @@ with a variety of different date implementations. However it also means that dev sure to provide the appropriate pieces for the datepicker to work with their chosen implementation. The easiest way to ensure this is just to import one of the pre-made modules: -|Module |Date type|Supported locales |Dependencies |Import from | -|---------------------|---------|-----------------------------------------------------------------------|----------------------------------|----------------------------------| -|`MatNativeDateModule`|`Date` |en-US |None |`@angular/material` | -|`MatMomentDateModule`|`Moment` |[See project](https://github.com/moment/moment/tree/develop/src/locale)|[Moment.js](https://momentjs.com/)|`@angular/material-moment-adapter`| +|Module |Date type |Supported locales |Dependencies |Import from | +|---------------------|-----------|-----------------------------------------------------------------------|----------------------------------------|----------------------------------| +|`MatNativeDateModule`|`Date` |en-US |None |`@angular/material` | +|`MatMomentDateModule`|`Moment` |[See project](https://github.com/moment/moment/tree/develop/src/locale)|[Moment.js](https://momentjs.com/) |`@angular/material-moment-adapter`| +|`MatLuxonDateModule` |`DateTime` |[See project](https://moment.github.io/luxon/docs/manual/intl.html) |[Luxon](https://moment.github.io/luxon/)|`@angular/material-luxon-adapter`| *Please note: `MatNativeDateModule` is based off of the functionality available in JavaScript's -native `Date` object, and is thus not suitable for many locales. One of the biggest shortcomings of -the native `Date` object is the inability to set the parse format. We highly recommend using the -`MomentDateAdapter` or a custom `DateAdapter` that works with the formatting/parsing library of your -choice.* +native `Date` object, and is thus not suitable for many locales. It is highly recommended to use +one of the alternative adapters or to create your own custom adapter* These modules include providers for `DateAdapter` and `MAT_DATE_FORMATS` diff --git a/src/lib/package.json b/src/lib/package.json index f9819adaed14..c25e1a13ef17 100644 --- a/src/lib/package.json +++ b/src/lib/package.json @@ -37,7 +37,8 @@ "packageGroup": [ "@angular/material", "@angular/cdk", - "@angular/material-moment-adapter" + "@angular/material-moment-adapter", + "@angular/material-luxon-adapter" ] }, "sideEffects": false diff --git a/src/material-examples/package.json b/src/material-examples/package.json index 958a86fc1e86..1e27e0bb3b50 100644 --- a/src/material-examples/package.json +++ b/src/material-examples/package.json @@ -27,7 +27,8 @@ "@angular/core": "0.0.0-NG", "@angular/common": "0.0.0-NG", "@angular/material": "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": "^1.7.1" diff --git a/src/material-examples/tsconfig-build.json b/src/material-examples/tsconfig-build.json index 609e5cffe333..e941d8e9ed59 100644 --- a/src/material-examples/tsconfig-build.json +++ b/src/material-examples/tsconfig-build.json @@ -31,6 +31,7 @@ "@angular/material-moment-adapter": ["../../dist/packages/material-moment-adapter"], "@angular/cdk/*": ["../../dist/packages/cdk/*"], "@angular/cdk-experimental/*": ["../../dist/packages/cdk-experimental/*"], + "@angular/material-luxon-adapter": ["../../dist/packages/material-luxon-adapter"] } }, "files": [ diff --git a/src/material-examples/tsconfig.json b/src/material-examples/tsconfig.json index e1156619275c..d4f515298190 100644 --- a/src/material-examples/tsconfig.json +++ b/src/material-examples/tsconfig.json @@ -10,7 +10,8 @@ "@angular/cdk/*": ["../cdk/*"], "@angular/material/*": ["../lib/*"], "@angular/material": ["../lib/public-api.ts"], - "@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/material-luxon-adapter/BUILD.bazel b/src/material-luxon-adapter/BUILD.bazel new file mode 100644 index 000000000000..acdbebbd1600 --- /dev/null +++ b/src/material-luxon-adapter/BUILD.bazel @@ -0,0 +1,50 @@ +package(default_visibility=["//visibility:public"]) + +load("//tools:defaults.bzl", "ng_module", "ng_package", "ng_web_test_suite", "ng_test_library") +load("//:packages.bzl", "ROLLUP_GLOBALS") + +ng_module( + name = "material-luxon-adapter", + srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]), + module_name = "@angular/material-luxon-adapter", + deps = [ + "@angular//packages/core", + "@matdeps//@types/luxon", + "//src/lib/core", + ], +) + +ng_test_library( + name = "luxon_adapter_test_sources", + srcs = glob(["**/*.spec.ts"]), + deps = [ + "//src/lib/core", + "//src/lib/testing", + "//src/cdk/platform", + "@matdeps//@types/luxon", + ":material-luxon-adapter", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [ + ":require-config.js", + ":luxon_adapter_test_sources" + ], + # 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 "ts_web_test_suite". + static_files = [ + "@matdeps//luxon", + ], +) + +ng_package( + name = "npm_package", + srcs = ["package.json"], + entry_point = "src/material-luxon-adapter/public_api.js", + globals = ROLLUP_GLOBALS, + deps = [":material-luxon-adapter"], + # TODO(devversion): re-enable once we have set up the proper compiler for the ng_package + tags = ["manual"], +) diff --git a/src/material-luxon-adapter/adapter/index.ts b/src/material-luxon-adapter/adapter/index.ts new file mode 100644 index 000000000000..42111d67850e --- /dev/null +++ b/src/material-luxon-adapter/adapter/index.ts @@ -0,0 +1,33 @@ +/** + * @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..00a5fa39fc9e --- /dev/null +++ b/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts @@ -0,0 +1,498 @@ +/** + * @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 {async, inject, TestBed} from '@angular/core/testing'; +import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; +import {Platform} from '@angular/cdk/platform'; +import {DateTime, Info, Features} from 'luxon'; +import {LuxonDateModule} from './index'; +import {MAT_LUXON_DATE_ADAPTER_OPTIONS, LuxonDateAdapter} from './luxon-date-adapter'; + +// Month constants used for more readable tests. We can't use the +// ones from `material/testing`, because Luxon's months start from 1. +const JAN = 1, FEB = 2, MAR = 3, DEC = 12; + +describe('LuxonDateAdapter', () => { + let adapter: LuxonDateAdapter; + let assertValidDate: (d: DateTime | null, valid: boolean) => void; + let platform: Platform; + let features: Features; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule] + }).compileComponents(); + })); + + beforeEach(inject([DateAdapter, Platform], (dateAdapter: LuxonDateAdapter, p: Platform) => { + adapter = dateAdapter; + platform = p; + features = Info.features(); + + adapter.setLocale('en-US'); + assertValidDate = (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'}`); + }; + })); + + 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 long 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'); + + if (features.intl && features.intlTokens) { + expect(adapter.getMonthNames('long')).toEqual([ + 'januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', + 'august', 'september', 'oktober', 'november', 'december' + ]); + } else { + expect(adapter.getMonthNames('long')).toEqual([ + 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', + 'October', '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('da-DK'); + + // Edge and IE support Intl, but have different data for Danish. + if (features.intl && !(platform.EDGE || platform.TRIDENT)) { + 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.' + ]); + } else { + 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([ + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' + ]); + }); + + it('should get short day of week names', () => { + expect(adapter.getDayOfWeekNames('short')).toEqual([ + 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' + ]); + }); + + it('should get narrow day of week names', () => { + expect(adapter.getDayOfWeekNames('narrow')).toEqual([ + 'M', 'T', 'W', 'T', 'F', 'S', 'S' + ]); + }); + + it('should get day of week names in a different locale', () => { + adapter.setLocale('ja-JP'); + + if (features.intl && features.intlTokens) { + expect(adapter.getDayOfWeekNames('long')).toEqual([ + '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日' + ]); + } else { + expect(adapter.getDayOfWeekNames('long')).toEqual([ + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' + ]); + } + }); + + 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 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'); + + if (platform.EDGE || platform.TRIDENT) { + expect(stripDirectionalityCharacters(date)).toEqual('2. jan 2017'); + } else { + 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.deserialize('1985-04-12T23:20:50.52Z'), true); + assertValidDate(adapter.deserialize('1996-12-19T16:39:57-08:00'), true); + assertValidDate(adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true); + assertValidDate(adapter.deserialize('1990-13-31T23:59:00Z'), false); + assertValidDate(adapter.deserialize('1/1/2017'), false); + expect(adapter.deserialize('')).toBeNull(); + expect(adapter.deserialize(null)).toBeNull(); + assertValidDate(adapter.deserialize(new Date()), true); + assertValidDate(adapter.deserialize(new Date(NaN)), false); + assertValidDate(adapter.deserialize(DateTime.local()), true); + assertValidDate(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.invalid(), false); + }); +}); + +describe('LuxonDateAdapter with MAT_DATE_LOCALE override', () => { + let adapter: LuxonDateAdapter; + let platform: Platform; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [{provide: MAT_DATE_LOCALE, useValue: 'da-DK'}] + }).compileComponents(); + })); + + beforeEach(inject([DateAdapter, Platform], (d: LuxonDateAdapter, p: Platform) => { + adapter = d; + platform = p; + })); + + it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => { + const date = adapter.format(DateTime.local(2017, JAN, 2), 'DD'); + + // Some browsers add extra invisible characters that we should strip before asserting. + if (platform.EDGE || platform.TRIDENT) { + // IE and Edge's format for Danish is slightly different. + expect(stripDirectionalityCharacters(date)).toEqual('2. jan 2017'); + } else { + expect(stripDirectionalityCharacters(date)).toEqual('2. jan. 2017'); + } + }); +}); + +describe('LuxonDateAdapter with LOCALE_ID override', () => { + let adapter: LuxonDateAdapter; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [{provide: LOCALE_ID, useValue: 'fr-FR'}] + }).compileComponents(); + })); + + beforeEach(inject([DateAdapter], (d: LuxonDateAdapter) => { + adapter = d; + })); + + 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: LuxonDateAdapter; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [{ + provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, + useValue: {useUtc: true} + }] + }).compileComponents(); + })); + + beforeEach(inject([DateAdapter], (d: LuxonDateAdapter) => { + adapter = d; + })); + + 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 stripDirectionalityCharacters(str: string) { + return str.replace(/[\u200e\u200f]/g, ''); +} 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..a0490252f915 --- /dev/null +++ b/src/material-luxon-adapter/adapter/luxon-date-adapter.ts @@ -0,0 +1,246 @@ +/** + * @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, Info, DateTimeOptions} 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 + }; +} + +/** The default date names to use if Intl API is not available. */ +const DEFAULT_DATE_NAMES = range(31, i => String(i + 1)); + +/** 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 ? !!options.useUtc : false; + this.setLocale(dateLocale || DateTime.local().locale); + } + + setLocale(locale: string) { + super.setLocale(locale); + } + + getYear(date: DateTime): number { + return date.year; + } + + getMonth(date: DateTime): number { + // Luxon works with 1-indexed months whereas our code expects 0-indexed. + return date.month - 1; + } + + getDate(date: DateTime): number { + return date.day; + } + + getDayOfWeek(date: DateTime): number { + return date.weekday; + } + + getMonthNames(style: 'long' | 'short' | 'narrow'): string[] { + return Info.months(style, {locale: this.locale}); + } + + getDateNames(): string[] { + if (Info.features().intl) { + // 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 = DateTime.utc(2017, 1, i + 1).toJSDate(); + + // Strip the directionality characters from the formatted date. + return dtf.format(date).replace(/[\u200e\u200f]/g, ''); + }); + } + return DEFAULT_DATE_NAMES; + } + + getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] { + return Info.weekdays(style, {locale: this.locale}); + } + + getYearName(date: DateTime): string { + return date.toFormat('yyyy'); + } + + getFirstDayOfWeek(): number { + // Luxon doesn't have support for getting the first day of the week. + return 0; + } + + getNumDaysInMonth(date: DateTime): number { + return date.daysInMonth; + } + + clone(date: DateTime): DateTime { + return DateTime.fromObject(date.toObject({includeConfig: true})); + } + + createDate(year: number, month: number, date: number): DateTime { + 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 ? DateTime.utc(year, month + 1, date) : DateTime.local(year, month + 1, date); + + if (!this.isValid(result)) { + throw Error(`Invalid date "${date}". Reason: "${result.invalidReason}".`); + } + + return result.setLocale(this.locale); + } + + today(): DateTime { + return (this._useUTC ? DateTime.utc() : DateTime.local()).setLocale(this.locale); + } + + parse(value: any, parseFormat: string): DateTime | null { + const options: DateTimeOptions = this._getOptions(); + + if (typeof value == 'string' && value.length > 0) { + const iso8601Date = DateTime.fromISO(value, options); + + if (this.isValid(iso8601Date)) { + return iso8601Date; + } + + const fromFormat = DateTime.fromFormat(value, parseFormat, options); + + if (this.isValid(fromFormat)) { + return fromFormat; + } + + return this.invalid(); + } else if (typeof value === 'number') { + return DateTime.fromMillis(value, options); + } else if (value instanceof Date) { + return DateTime.fromJSDate(value, options); + } else if (value instanceof DateTime) { + return DateTime.fromMillis(value.toMillis(), options); + } + + return null; + } + + format(date: DateTime, 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: DateTime, years: number): DateTime { + return date.plus({years}).setLocale(this.locale); + } + + addCalendarMonths(date: DateTime, months: number): DateTime { + return date.plus({months}).setLocale(this.locale); + } + + addCalendarDays(date: DateTime, days: number): DateTime { + return date.plus({days}).setLocale(this.locale); + } + + toIso8601(date: DateTime): 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. + */ + deserialize(value: any): DateTime | null { + const options = this._getOptions(); + let date; + if (value instanceof Date) { + date = DateTime.fromJSDate(value, options); + } + if (typeof value === 'string') { + if (!value) { + return null; + } + date = DateTime.fromISO(value, options); + } + if (date && this.isValid(date)) { + return date; + } + return super.deserialize(value); + } + + isDateInstance(obj: any): boolean { + return obj instanceof DateTime; + } + + isValid(date: DateTime): boolean { + return date.isValid; + } + + invalid(): DateTime { + return DateTime.invalid('Invalid Luxon DateTime object.'); + } + + /** Gets the options that should be used when constructing a new `DateTime` object. */ + private _getOptions(): DateTimeOptions { + 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..fdb094f15e30 --- /dev/null +++ b/src/material-luxon-adapter/adapter/luxon-date-formats.ts @@ -0,0 +1,23 @@ +/** + * @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: { + // Note: this isn't localized like `D`, however Luxon can't parse input against `D`. + dateInput: 'L/d/yyyy', + }, + 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..4889ad4bcbe6 --- /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", + "main": "./bundles/material-luxon-adapter.umd.js", + "module": "./esm5/material-luxon-adapter.es5.js", + "es2015": "./esm2015/material-luxon-adapter.js", + "typings": "./material-luxon-adapter.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/angular/material2.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/angular/material2/issues" + }, + "homepage": "https://github.com/angular/material2#readme", + "peerDependencies": { + "@angular/material": "0.0.0-PLACEHOLDER", + "@angular/core": "0.0.0-NG", + "luxon": "^1.8.3" + }, + "dependencies": { + "tslib": "^1.7.1" + }, + "ng-update": { + "packageGroup": [ + "@angular/material", + "@angular/cdk", + "@angular/material-luxon-adapter" + ] + } +} 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..4a07f67c8d48 --- /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/matdeps/node_modules/luxon/build/amd/luxon' + } +}); diff --git a/src/material-luxon-adapter/tsconfig-build.json b/src/material-luxon-adapter/tsconfig-build.json new file mode 100644 index 000000000000..5429fdeb816b --- /dev/null +++ b/src/material-luxon-adapter/tsconfig-build.json @@ -0,0 +1,32 @@ +// TypeScript config file that is used to compile the cdk's ES2015 package through Gulp. As the +// long term goal is to switch to Bazel, and we already want to run tests with Bazel, we need to +// ensure the TypeScript build options are the same for Gulp and Bazel. We achieve this by +// extending the generic Bazel build tsconfig which will be used for each entry-point. +{ + "extends": "../bazel-tsconfig-build.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "../../dist/packages/material-luxon-adapter", + "rootDir": ".", + "rootDirs": [ + ".", + "../../dist/packages/material-luxon-adapter" + ], + "paths": { + "@angular/material/*": ["../../dist/packages/material/*"], + "@angular/material": ["../../dist/packages/material"], + "@angular/cdk/*": ["../../dist/packages/cdk/*"] + } + }, + "files": [ + "public-api.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/material-luxon-adapter", + "skipTemplateCodegen": true, + "fullTemplateTypeCheck": true + } +} diff --git a/src/material-luxon-adapter/tsconfig-tests.json b/src/material-luxon-adapter/tsconfig-tests.json new file mode 100644 index 000000000000..e776066105ae --- /dev/null +++ b/src/material-luxon-adapter/tsconfig-tests.json @@ -0,0 +1,28 @@ +// TypeScript config file that extends the default tsconfig file for the cdk. This config is +// used to compile the tests for Karma. Since the code will run inside of the browser, the target +// needs to be ES5. The format needs to be CommonJS since Karma only supports that module format. +{ + "extends": "./tsconfig-build", + "compilerOptions": { + "importHelpers": false, + "module": "commonjs", + "target": "es5", + "types": ["jasmine"], + "experimentalDecorators": true + }, + "angularCompilerOptions": { + "strictMetadataEmit": true, + "skipTemplateCodegen": true, + "emitDecoratorMetadata": true, + "fullTemplateTypeCheck": true, + + // Unset options inherited from tsconfig-build + "annotateForClosureCompiler": false, + "flatModuleOutFile": null, + "flatModuleId": null + }, + "include": [ + "**/*.spec.ts", + "index.ts" + ] +} diff --git a/src/material-luxon-adapter/tsconfig.json b/src/material-luxon-adapter/tsconfig.json new file mode 100644 index 000000000000..f7f702c1c807 --- /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/*": ["../lib/*"], + "@angular/material": ["../lib/public-api.ts"] + } + }, + "include": ["./**/*.ts"] +} diff --git a/test/karma-system-config.js b/test/karma-system-config.js index 3b19c85b9f05..3a296913c069 100644 --- a/test/karma-system-config.js +++ b/test/karma-system-config.js @@ -8,6 +8,7 @@ System.config({ 'rxjs': 'node:rxjs', 'tslib': 'node:tslib/tslib.js', 'moment': 'node:moment/min/moment-with-locales.min.js', + 'luxon': 'node:luxon/build/amd/luxon.js', // Angular specific mappings. '@angular/core': 'node:@angular/core/bundles/core.umd.js', diff --git a/test/karma.conf.js b/test/karma.conf.js index a09d66cd45b4..95d1d993ae7a 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -38,6 +38,7 @@ module.exports = config => { {pattern: 'node_modules/zone.js/dist/fake-async-test.js', included: true, watched: false}, {pattern: 'node_modules/hammerjs/hammer.min.js', included: true, watched: false}, {pattern: 'node_modules/moment/min/moment-with-locales.min.js', included: false, watched: false}, + {pattern: 'node_modules/luxon/build/amd/luxon.js', included: false, watched: false}, // Include all Angular dependencies {pattern: 'node_modules/@angular/**/*', included: false, watched: false}, diff --git a/tools/gulp/gulpfile.ts b/tools/gulp/gulpfile.ts index eef231f298c6..0cc59f92a3e0 100644 --- a/tools/gulp/gulpfile.ts +++ b/tools/gulp/gulpfile.ts @@ -7,7 +7,8 @@ import { examplesPackage, materialExperimentalPackage, materialPackage, - momentAdapterPackage + momentAdapterPackage, + luxonAdapterPackage } from './packages'; createPackageBuildTasks(cdkPackage); @@ -16,6 +17,7 @@ createPackageBuildTasks(materialPackage); createPackageBuildTasks(materialExperimentalPackage); createPackageBuildTasks(examplesPackage, ['build-examples-module']); createPackageBuildTasks(momentAdapterPackage); +createPackageBuildTasks(luxonAdapterPackage); import './tasks/aot'; import './tasks/breaking-changes'; diff --git a/tools/gulp/packages.ts b/tools/gulp/packages.ts index 00df72ffb62b..49d1c3d17f66 100644 --- a/tools/gulp/packages.ts +++ b/tools/gulp/packages.ts @@ -7,10 +7,12 @@ export const cdkExperimentalPackage = new BuildPackage('cdk-experimental', [cdkP export const materialExperimentalPackage = new BuildPackage('material-experimental', [materialPackage]); export const momentAdapterPackage = new BuildPackage('material-moment-adapter', [materialPackage]); +export const luxonAdapterPackage = new BuildPackage('material-luxon-adapter', [materialPackage]); export const examplesPackage = new BuildPackage('material-examples', [ cdkPackage, materialPackage, - momentAdapterPackage + momentAdapterPackage, + luxonAdapterPackage ]); // The material package re-exports its secondary entry-points at the root so that all of the @@ -36,5 +38,6 @@ export const allBuildPackages = [ cdkExperimentalPackage, materialExperimentalPackage, momentAdapterPackage, + luxonAdapterPackage, examplesPackage ]; diff --git a/tools/gulp/tasks/aot.ts b/tools/gulp/tasks/aot.ts index e3e5bd8f9d6d..04451f35780c 100644 --- a/tools/gulp/tasks/aot.ts +++ b/tools/gulp/tasks/aot.ts @@ -35,6 +35,7 @@ task('build-aot:release-packages', sequenceTask( 'cdk-experimental:build-release', 'material-experimental:build-release', 'material-moment-adapter:build-release', + 'material-luxon-adapter:build-release', 'material-examples:build-release', ], )); diff --git a/tools/gulp/tasks/development.ts b/tools/gulp/tasks/development.ts index f24b692e5ce6..b7dc194dee77 100644 --- a/tools/gulp/tasks/development.ts +++ b/tools/gulp/tasks/development.ts @@ -15,6 +15,7 @@ import { materialPackage, momentAdapterPackage, examplesPackage, + luxonAdapterPackage, } from '../packages'; import {watchFilesAndReload} from '../util/watch-files-reload'; @@ -38,6 +39,7 @@ const appVendors = [ 'hammerjs', 'core-js', 'moment', + 'luxon', 'tslib', '@webcomponents', ]; @@ -64,6 +66,7 @@ task('build:devapp', sequenceTask( 'cdk-experimental:build-no-bundles', 'material-experimental:build-no-bundles', 'material-moment-adapter:build-no-bundles', + 'material-luxon-adapter:build-no-bundles', 'build-examples-module', // The examples module needs to be manually built before building examples package because // when using the `no-bundles` task, the package-specific pre-build tasks won't be executed. @@ -102,6 +105,8 @@ task('stage-deploy:devapp', ['build:devapp'], () => { join(deployOutputDir, 'dist/packages/material-examples')); copyFiles(momentAdapterPackage.outputDir, '**/*.+(js|map)', join(deployOutputDir, 'dist/packages/material-moment-adapter')); + copyFiles(luxonAdapterPackage.outputDir, '**/*.+(js|map)', + join(deployOutputDir, 'dist/packages/material-luxon-adapter')); }); /** @@ -155,6 +160,10 @@ task(':watch:devapp', () => { watchFilesAndReload(join(momentAdapterPackage.sourceDir, '**/*'), ['material-moment-adapter:build-no-bundles']); + // Luxon adapter package watchers + watchFilesAndReload(join(luxonAdapterPackage.sourceDir, '**/*'), + ['material-luxon-adapter:build-no-bundles']); + // Material experimental package watchers watchFilesAndReload(join(materialExperimentalPackage.sourceDir, '**/*'), ['material-experimental:build-no-bundles']); diff --git a/tools/gulp/tasks/unit-test.ts b/tools/gulp/tasks/unit-test.ts index 074de0457c9b..257750dc85c0 100644 --- a/tools/gulp/tasks/unit-test.ts +++ b/tools/gulp/tasks/unit-test.ts @@ -19,7 +19,8 @@ task(':test:build', sequenceTask( 'material:build-no-bundles', 'cdk-experimental:build-no-bundles', 'material-experimental:build-no-bundles', - 'material-moment-adapter:build-no-bundles' + 'material-moment-adapter:build-no-bundles', + 'material-luxon-adapter:build-no-bundles' )); /** diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index b4aca4062e58..8e1dfc7e3f89 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -39,6 +39,7 @@ const rollupCdkExperimentalEntryPoints = /** Map of globals that are used inside of the different packages. */ export const rollupGlobals = { 'moment': 'moment', + 'luxon': 'luxon', 'tslib': 'tslib', '@angular/animations': 'ng.animations', @@ -63,6 +64,7 @@ export const rollupGlobals = { '@angular/material-examples': 'ng.materialExamples', '@angular/material-experimental': 'ng.materialExperimental', '@angular/material-moment-adapter': 'ng.materialMomentAdapter', + '@angular/material-luxon-adapter': 'ng.materialLuxonAdapter', // Include secondary entry-points of the cdk and material packages ...rollupCdkEntryPoints, diff --git a/tools/release/release-output/release-packages.ts b/tools/release/release-output/release-packages.ts index 2dc47f7b4dea..3a64c1c8c124 100644 --- a/tools/release/release-output/release-packages.ts +++ b/tools/release/release-output/release-packages.ts @@ -4,5 +4,6 @@ export const releasePackages = [ 'material', 'cdk-experimental', 'material-experimental', - 'material-moment-adapter' + 'material-moment-adapter', + 'material-luxon-adapter' ]; diff --git a/yarn.lock b/yarn.lock index 0ebbd1f40704..2c65340d9c0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -721,6 +721,11 @@ resolved "https://registry.yarnpkg.com/@types/jju/-/jju-1.4.1.tgz#0a39f5f8e84fec46150a7b9ca985c3f89ad98e9f" integrity sha512-LFt+YA7Lv2IZROMwokZKiPNORAV5N3huMs3IKnzlE430HWhWYZ8b+78HiwJXJJP1V2IEjinyJURuRJfGoaFSIA== +"@types/luxon@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.4.1.tgz#ece499ada215fb253e07c7ac05d30731412ba6aa" + integrity sha512-mYv/gbkOJ40CDgR8st5sosfFNrJncdlkpdzQSNRdU86UQg3oWWmll4AO/7B8F5FlBC6YrIXqXDSnkoCBqo+uMA== + "@types/marked@^0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.4.2.tgz#64a89e53ea37f61cc0f3ee1732c555c2dbf6452f" @@ -6937,6 +6942,11 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +luxon@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.8.3.tgz#6c76b50319b3a91a357002ca2e96576bb3bfe26d" + integrity sha512-hICQcGmUl5e9VUnf5PtHAma+tpiwL/wMBV9/6/M/6iSpzWFHdEElK2Rj4IZx8kbk2/baJvYftgb+n/I2wJ7RXQ== + macos-release@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.0.0.tgz#7dddf4caf79001a851eb4fba7fb6034f251276ab"