diff --git a/package.json b/package.json index 9f6b1bfbc2..1e5da77a79 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "bootstrap": "4.0.0", "colors.js": "1.2.4", "core-js": "2.5.7", - "date-fns": "^2.0.0-alpha.16", + "date-fns": ">=2.0.0-alpha.16 <=2.0.0-alpha.27", "docsearch.js": "^2.5.2", "gulp-bump": "2.7.0", "highlight.js": "9.12.0", diff --git a/scripts/gulp/tasks/bundle/rollup-config.ts b/scripts/gulp/tasks/bundle/rollup-config.ts index b29c0c2e4c..6547b4ddc4 100644 --- a/scripts/gulp/tasks/bundle/rollup-config.ts +++ b/scripts/gulp/tasks/bundle/rollup-config.ts @@ -34,6 +34,7 @@ const ROLLUP_GLOBALS = { // date libs for date service 'moment': 'moment', 'date-fns/parse': 'date-fns.parse', + 'date-fns/format': 'date-fns.format', // @nebular dependencies '@nebular/theme': 'nb.theme', diff --git a/src/framework/date-fns/date-fns.module.ts b/src/framework/date-fns/date-fns.module.ts index 0d59f6052f..8a2467d42c 100644 --- a/src/framework/date-fns/date-fns.module.ts +++ b/src/framework/date-fns/date-fns.module.ts @@ -4,14 +4,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { NgModule } from '@angular/core'; +import { ModuleWithProviders, NgModule } from '@angular/core'; -import { NbDateService } from '@nebular/theme'; -import { NbDateFnsDateService } from './services/date-fns-date.service'; +import { NB_DATE_SERVICE_OPTIONS, NbDateService } from '@nebular/theme'; +import { NbDateFnsOptions, NbDateFnsDateService } from './services/date-fns-date.service'; +const dateFnsServiceProvider = { provide: NbDateService, useClass: NbDateFnsDateService }; @NgModule({ - providers: [{ provide: NbDateService, useClass: NbDateFnsDateService }], + providers: [ dateFnsServiceProvider ], }) export class NbDateFnsDateModule { + static forRoot(options: Partial): ModuleWithProviders { + return { + ngModule: NbDateFnsDateModule, + providers: [ + dateFnsServiceProvider, + { provide: NB_DATE_SERVICE_OPTIONS, useValue: options }, + ], + }; + } + + static forChild(options: Partial): ModuleWithProviders { + return { + ngModule: NbDateFnsDateModule, + providers: [ + dateFnsServiceProvider, + { provide: NB_DATE_SERVICE_OPTIONS, useValue: options }, + ], + }; + } } diff --git a/src/framework/date-fns/package.json b/src/framework/date-fns/package.json index 8026a45f0f..fecbd1fc52 100644 --- a/src/framework/date-fns/package.json +++ b/src/framework/date-fns/package.json @@ -25,6 +25,6 @@ ], "peerDependencies": { "@nebular/theme": "3.2.1", - "date-fns": "^2.0.0-alpha.16" + "date-fns": ">=2.0.0-alpha.16 <=2.0.0-alpha.27" } } \ No newline at end of file diff --git a/src/framework/date-fns/services/date-fns-date.service.spec.ts b/src/framework/date-fns/services/date-fns-date.service.spec.ts index 9ffad7413c..c1348f6bb1 100644 --- a/src/framework/date-fns/services/date-fns-date.service.spec.ts +++ b/src/framework/date-fns/services/date-fns-date.service.spec.ts @@ -11,17 +11,66 @@ import { NbDateService } from '@nebular/theme'; import { NbDateFnsDateService } from './date-fns-date.service'; - describe('date-fns-date-service', () => { let dateService: NbDateService; beforeEach(() => { TestBed.configureTestingModule({}); - dateService = new NbDateFnsDateService(TestBed.get(LOCALE_ID)); + dateService = new NbDateFnsDateService(TestBed.get(LOCALE_ID), null); }); it('should parse date according to the MM.dd.yyyy format', () => { const date = '06.15.2018'; expect(dateService.parse(date, 'MM.dd.yyyy')).toEqual(new Date(2018, 5, 15)); }); + + describe('service global config', () => { + const SEPARATOR = '_'; + const FORMAT = `MM${SEPARATOR}dd${SEPARATOR}yyyy`; + const year = 2010; + const monthIndex = 10; + const month = monthIndex + 1; + const day = 20; + const date = new Date(year, monthIndex, day); + const formattedDate = `${month}${SEPARATOR}${day}${SEPARATOR}${year}`; + + beforeEach(() => { + dateService = new NbDateFnsDateService( + TestBed.get(LOCALE_ID), + { + format: FORMAT, + parseOptions: { awareOfUnicodeTokens: true }, + formatOptions: { awareOfUnicodeTokens: true }, + }, + ); + }); + + it('should use format from global config if isn\'t passed as parameter', () => { + expect(dateService.format(date, undefined)).toEqual(formattedDate); + + const parsedDate = dateService.parse(formattedDate, FORMAT); + expect(parsedDate.valueOf()).toEqual(date.valueOf()); + }); + + it('should use parameter over global config format if presented', () => { + expect(dateService.format(date, undefined)).toEqual(formattedDate); + + const parsedDate = dateService.parse(formattedDate, FORMAT); + expect(parsedDate.valueOf()).toEqual(date.valueOf()); + }); + + it('should pass parseOptions to parse function', () => { + // date-fns require { awareOfUnicodeTokens: true } option to be passed to parse function + // when format contains 'DD' or 'YYYY' tokens, otherwise it throws. This option is + // passed as global config to service constructor so it shouldn't throw. + expect(() => dateService.parse(formattedDate, 'DD/MM/YYYY')).not.toThrow(); + }); + + it('should pass formatOptions to format function', () => { + // date-fns require { awareOfUnicodeTokens: true } option to be passed to format function + // when format contains 'DD' or 'YYYY' tokens, otherwise it throws. This option is + // passed as global config to service constructor so it shouldn't throw. + expect(() => dateService.format(date, 'DD/MM/YYYY')).not.toThrow(); + }); + }); }); diff --git a/src/framework/date-fns/services/date-fns-date.service.ts b/src/framework/date-fns/services/date-fns-date.service.ts index 25ef16e217..02bb3a573b 100644 --- a/src/framework/date-fns/services/date-fns-date.service.ts +++ b/src/framework/date-fns/services/date-fns-date.service.ts @@ -4,26 +4,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { Inject, Injectable, LOCALE_ID } from '@angular/core'; +import { Inject, Injectable, LOCALE_ID, Optional } from '@angular/core'; -import { NbNativeDateService } from '@nebular/theme'; +import { NB_DATE_SERVICE_OPTIONS, NbNativeDateService } from '@nebular/theme'; import * as dateFnsParse from 'date-fns/parse'; // @ts-ignore -import { default as rollupParse} from 'date-fns/parse'; +import { default as rollupParse } from 'date-fns/parse'; +import * as dateFnsFormat from 'date-fns/format'; +// @ts-ignore +import { default as rollupFormat } from 'date-fns/format'; const parse = rollupParse || dateFnsParse; +const formatDate = rollupFormat || dateFnsFormat; +export interface NbDateFnsOptions { + format: string; + parseOptions: {}, + formatOptions: {}, +} @Injectable() export class NbDateFnsDateService extends NbNativeDateService { - constructor(@Inject(LOCALE_ID) locale: string) { + protected options: Partial; + + constructor( + @Inject(LOCALE_ID) locale: string, + @Optional() @Inject(NB_DATE_SERVICE_OPTIONS) options, + ) { super(locale); this.setLocale(locale); + this.options = options || {}; + } + + format(date: Date, format: string): string { + return formatDate(date, format || this.options.format, this.options.formatOptions); } parse(date: string, format: string): Date { - return parse(date, format, new Date()); + return parse(date, format || this.options.format, new Date(), this.options.parseOptions); } getId(): string { diff --git a/src/framework/theme/components/datepicker/datepicker.component.ts b/src/framework/theme/components/datepicker/datepicker.component.ts index dd26e28cdd..6998e698be 100644 --- a/src/framework/theme/components/datepicker/datepicker.component.ts +++ b/src/framework/theme/components/datepicker/datepicker.component.ts @@ -16,9 +16,13 @@ import { OnDestroy, Output, Type, + AfterViewInit, + OnInit, + SimpleChanges, + Optional, } from '@angular/core'; import { takeWhile } from 'rxjs/operators'; -import { Observable, Subject } from 'rxjs'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; import { NbAdjustableConnectedPositionStrategy, @@ -43,13 +47,15 @@ import { NbCalendarViewMode, NbDateService, } from '../calendar-kit'; -import { NbDatepicker, NbPickerValidatorConfig } from './datepicker.directive'; +import { NB_DATE_SERVICE_OPTIONS, NbDatepicker, NbPickerValidatorConfig } from './datepicker.directive'; /** * The `NbBasePicker` component concentrates overlay manipulation logic. * */ -export abstract class NbBasePicker extends NbDatepicker implements OnChanges, OnDestroy { +export abstract class NbBasePicker + extends NbDatepicker + implements OnInit, OnChanges, AfterViewInit, OnDestroy { /** * Datepicker date format. Can be used only with date adapters (moment, date-fns) since native date * object doesn't support formatting. @@ -139,6 +145,8 @@ export abstract class NbBasePicker extends NbDatepicker implements O * */ protected hostRef: ElementRef; + protected init$: ReplaySubject = new ReplaySubject(); + /** * Stream of picker changes. Required to be the subject because picker hides and shows and picker * change stream becomes recreated. @@ -156,7 +164,7 @@ export abstract class NbBasePicker extends NbDatepicker implements O * Queue contains the last value that was applied to the picker when it was hidden. * This value will be passed to the picker as soon as it shown. * */ - protected queue: T; + protected queue: T | undefined; protected blur$: Subject = new Subject(); @@ -166,6 +174,7 @@ export abstract class NbBasePicker extends NbDatepicker implements O protected overlay: NbOverlayService, protected cfr: ComponentFactoryResolver, protected dateService: NbDateService, + @Optional() @Inject(NB_DATE_SERVICE_OPTIONS) protected dateServiceOptions, ) { super(); } @@ -188,6 +197,10 @@ export abstract class NbBasePicker extends NbDatepicker implements O return this.ref && this.ref.hasAttached(); } + get init(): Observable { + return this.init$.asObservable(); + } + /** * Emits when datepicker looses focus. */ @@ -197,18 +210,24 @@ export abstract class NbBasePicker extends NbDatepicker implements O protected abstract get pickerValueChange(): Observable; - ngOnChanges() { - if (this.dateService.getId() === 'native' && this.format) { - throw new Error('Can\'t format native date. To use custom formatting you have to install @nebular/moment or ' + - '@nebular/date-fns package and import NbMomentDateModule or NbDateFnsDateModule accordingly.' + - 'More information at "Formatting issue" ' + - 'https://akveo.github.io/nebular/docs/components/datepicker/overview#nbdatepickercomponent'); + ngOnInit() { + this.checkFormat(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.format && !changes.format.isFirstChange()) { + this.checkFormat(); } } + ngAfterViewInit() { + this.init$.next(); + } + ngOnDestroy() { this.alive = false; this.hide(); + this.init$.complete(); if (this.ref) { this.ref.dispose(); @@ -328,6 +347,20 @@ export abstract class NbBasePicker extends NbDatepicker implements O this.picker.size = this.size; this.picker.visibleDate = this.visibleDate; } + + protected checkFormat() { + if (this.dateService.getId() === 'native' && this.format) { + throw new Error('Can\'t format native date. To use custom formatting you have to install @nebular/moment or ' + + '@nebular/date-fns package and import NbMomentDateModule or NbDateFnsDateModule accordingly.' + + 'More information at "Formatting issue" ' + + 'https://akveo.github.io/nebular/docs/components/datepicker/overview#nbdatepickercomponent'); + } + + const isFormatSet = this.format || (this.dateServiceOptions && this.dateServiceOptions.format); + if (this.dateService.getId() === 'date-fns' && !isFormatSet) { + throw new Error('format is required when using NbDateFnsDateModule'); + } + } } /** @@ -355,8 +388,8 @@ export class NbDatepickerComponent extends NbBasePicker; } - get value(): D { - return this.picker.date; + get value(): D | undefined { + return this.picker ? this.picker.date : undefined; } set value(date: D) { @@ -405,8 +438,8 @@ export class NbRangepickerComponent extends NbBasePicker>; } - get value(): NbCalendarRange { - return this.picker.range; + get value(): NbCalendarRange | undefined { + return this.picker ? this.picker.range : undefined; } set value(range: NbCalendarRange) { @@ -426,7 +459,7 @@ export class NbRangepickerComponent extends NbBasePicker { * */ abstract format: string; - abstract get value(): T; + abstract get value(): T | undefined; abstract set value(value: T); abstract get valueChange(): Observable; + abstract get init(): Observable; + /** * Attaches datepicker to the native input element. * */ @@ -107,6 +118,7 @@ export abstract class NbDatepicker { export const NB_DATE_ADAPTER = new InjectionToken>('Datepicker Adapter'); +export const NB_DATE_SERVICE_OPTIONS = new InjectionToken('Date service options'); /** * The `NbDatepickerDirective` is form control that gives you ability to select dates and ranges. The datepicker @@ -185,6 +197,41 @@ export const NB_DATE_ADAPTER = new InjectionToken>('Dat * Because date-fns is treeshakable, tiny and operates native date objects. If you want to use it you have to * install it: `npm i @nebular/date-fns`, and import `NbDateFnsDateModule` from this package. * + * ### NbDateFnsDateModule + * + * Format is required when using `NbDateFnsDateModule`. You can set it via `format` input on datepicker component: + * ```html + * + * ``` + * Also format can be set globally with `NbDateFnsDateModule.forRoot({ format: 'dd.MM.yyyy' })` and + * `NbDateFnsDateModule.forChild({ format: 'dd.MM.yyyy' })` methods. + * + * Please note to use some of the formatting tokens you also need to pass `{ awareOfUnicodeTokens: true }` to date-fns + * parse and format functions. You can configure options passed this functions by setting `formatOptions` and + * `parseOptions` of options object passed to `NbDateFnsDateModule.forRoot` and `NbDateFnsDateModule.forChild` methods. + * ```ts + * NbDateFnsDateModule.forRoot({ + * parseOptions: { awareOfUnicodeTokens: true }, + * formatOptions: { awareOfUnicodeTokens: true }, + * }) + * ``` + * Further info on `date-fns` formatting tokens could be found at + * [date-fns docs](https://date-fns.org/v2.0.0-alpha.27/docs/Unicode-Tokens). + * + * You can also use `parseOptions` and `formatOptions` to provide locale. + * ```ts + * import { eo } from 'date-fns/locale'; + * + * @NgModule({ + * imports: [ + * NbDateFnsDateModule.forRoot({ + * parseOptions: { locale: eo }, + * formatOptions: { locale: eo }, + * }), + * ], + * }) + * ``` + * * @styles * * datepicker-fg @@ -229,6 +276,8 @@ export class NbDatepickerDirective implements OnDestroy, ControlValueAccessor * */ protected picker: NbDatepicker; protected alive: boolean = true; + protected isDatepickerReady: boolean = false; + protected queue: D | undefined; protected onChange: (D) => void = () => {}; protected onTouched: () => void = () => {}; @@ -245,7 +294,8 @@ export class NbDatepickerDirective implements OnDestroy, ControlValueAccessor constructor(@Inject(NB_DOCUMENT) protected document, @Inject(NB_DATE_ADAPTER) protected datepickerAdapters: NbDatepickerAdapter[], protected hostRef: ElementRef, - protected dateService: NbDateService) { + protected dateService: NbDateService, + protected changeDetector: ChangeDetectorRef) { this.subscribeOnInputChange(); } @@ -271,8 +321,12 @@ export class NbDatepickerDirective implements OnDestroy, ControlValueAccessor * Writes value in picker and html input element. * */ writeValue(value: D) { - this.writePicker(value); - this.writeInput(value); + if (this.isDatepickerReady) { + this.writePicker(value); + this.writeInput(value); + } else { + this.queue = value; + } } registerOnChange(fn: any): void { @@ -358,8 +412,27 @@ export class NbDatepickerDirective implements OnDestroy, ControlValueAccessor this.chooseDatepickerAdapter(); this.picker.attach(this.hostRef); - if (this.hostRef.nativeElement.value) { - this.picker.value = this.datepickerAdapter.parse(this.hostRef.nativeElement.value, this.picker.format); + if (this.inputValue) { + this.picker.value = this.datepickerAdapter.parse(this.inputValue, this.picker.format); + } + + // In case datepicker component placed after the input with datepicker directive, + // we can't read `this.picker.format` on first change detection run, + // since it's not bound yet, so we have to wait for datepicker component initialization. + if (!this.isDatepickerReady) { + this.picker.init + .pipe( + takeWhile(() => this.alive), + take(1), + tap(() => this.isDatepickerReady = true), + filter(() => !!this.queue), + ) + .subscribe(() => { + this.writeValue(this.queue); + this.onChange(this.queue); + this.changeDetector.detectChanges(); + this.queue = undefined; + }); } this.picker.valueChange