From 1d115f88929ef9a7736a99ba45523db2168fc322 Mon Sep 17 00:00:00 2001 From: Cory Rylan <2021067+coryrylan@users.noreply.github.com> Date: Tue, 22 Jan 2019 09:54:33 -0600 Subject: [PATCH] [NG] fix input reset on datepicker/refactor (#3050) * [NG] refactor date input, add tests Signed-off-by: Cory Rylan * [NG] fix input reset on datepicker When using clrDate, setting input to null would not clear the input. closes #3012 Signed-off-by: Cory Rylan --- golden/clr-angular.d.ts | 4 +- .../forms/datepicker/date-input.spec.ts | 23 ++ .../forms/datepicker/date-input.ts | 362 +++++++----------- .../providers/date-io.service.spec.ts | 42 +- .../datepicker/providers/date-io.service.ts | 5 +- .../datepicker-focus.service.spec.ts | 4 +- .../providers/datepicker-focus.service.ts | 4 + .../forms/datepicker/utils/date-utils.spec.ts | 34 ++ .../forms/datepicker/utils/date-utils.ts | 12 + 9 files changed, 239 insertions(+), 251 deletions(-) create mode 100644 src/clr-angular/forms/datepicker/utils/date-utils.spec.ts diff --git a/golden/clr-angular.d.ts b/golden/clr-angular.d.ts index 47d64c994f..8d0c66e2ef 100644 --- a/golden/clr-angular.d.ts +++ b/golden/clr-angular.d.ts @@ -511,10 +511,10 @@ export declare class ClrDateContainer implements DynamicWrapper, OnDestroy { } export declare class ClrDateInput extends WrappedFormControl implements OnInit, AfterViewInit, OnDestroy { - _dateUpdated: EventEmitter; clrNewLayout: boolean; protected control: NgControl; date: Date; + dateChange: EventEmitter; protected el: ElementRef; protected index: number; readonly inputType: string; @@ -522,7 +522,7 @@ export declare class ClrDateInput extends WrappedFormControl i placeholder: string; readonly placeholderText: string; protected renderer: Renderer2; - constructor(vcr: ViewContainerRef, injector: Injector, el: ElementRef, renderer: Renderer2, control: NgControl, container: ClrDateContainer, _dateIOService: DateIOService, _dateNavigationService: DateNavigationService, _datepickerEnabledService: DatepickerEnabledService, dateFormControlService: DateFormControlService, platformId: Object, focusService: FocusService, newFormsLayout: boolean, datepickerFocusService: DatepickerFocusService); + constructor(viewContainerRef: ViewContainerRef, injector: Injector, el: ElementRef, renderer: Renderer2, control: NgControl, container: ClrDateContainer, dateIOService: DateIOService, dateNavigationService: DateNavigationService, datepickerEnabledService: DatepickerEnabledService, dateFormControlService: DateFormControlService, platformId: Object, focusService: FocusService, newFormsLayout: boolean, datepickerFocusService: DatepickerFocusService); ngAfterViewInit(): void; ngOnInit(): void; onValueChange(target: HTMLInputElement): void; diff --git a/src/clr-angular/forms/datepicker/date-input.spec.ts b/src/clr-angular/forms/datepicker/date-input.spec.ts index 19696e09d2..b3b5357c99 100644 --- a/src/clr-angular/forms/datepicker/date-input.spec.ts +++ b/src/clr-angular/forms/datepicker/date-input.spec.ts @@ -580,6 +580,29 @@ export default function() { expect(fixture.componentInstance.date).toBeNull(); }); + + it('preserves input value from user when date is invalid', () => { + dateInputDebugElement.nativeElement.value = '01/02/201'; + dateInputDebugElement.nativeElement.dispatchEvent(new Event('change')); + fixture.detectChanges(); + + expect(fixture.componentInstance.date).toBe(null); + expect(dateInputDebugElement.nativeElement.value).toBe('01/02/201'); + }); + + it('updates the HTML input with value from clrDate input', () => { + expect(fixture.componentInstance.date).toBeUndefined(); + fixture.detectChanges(); + expect(dateInputDebugElement.nativeElement.value).toBe(''); + + fixture.componentInstance.date = new Date(2019, 2, 1); + fixture.detectChanges(); + expect(dateInputDebugElement.nativeElement.value).toBe('03/01/2019'); + + fixture.componentInstance.date = null; + fixture.detectChanges(); + expect(dateInputDebugElement.nativeElement.value).toBe(''); + }); }); }); } diff --git a/src/clr-angular/forms/datepicker/date-input.ts b/src/clr-angular/forms/datepicker/date-input.ts index 0f2197a88f..5ef4d13602 100644 --- a/src/clr-angular/forms/datepicker/date-input.ts +++ b/src/clr-angular/forms/datepicker/date-input.ts @@ -25,7 +25,8 @@ import { ViewContainerRef, } from '@angular/core'; import { NgControl } from '@angular/forms'; -import { filter } from 'rxjs/operators'; +import { filter, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; import { FocusService } from '../common/providers/focus.service'; import { WrappedFormControl } from '../common/wrapped-control'; @@ -37,6 +38,13 @@ import { DateNavigationService } from './providers/date-navigation.service'; import { DatepickerEnabledService } from './providers/datepicker-enabled.service'; import { IS_NEW_FORMS_LAYOUT } from '../common/providers/new-forms.service'; import { DatepickerFocusService } from './providers/datepicker-focus.service'; +import { datesAreEqual } from './utils/date-utils'; + +// There are four ways the datepicker value is set +// 1. Value set by user typing into text input as a string ex: '01/28/2015' +// 2. Value set explicitly by Angular Forms APIs as a string ex: '01/28/2015' +// 3. Value set by user via datepicker UI as a Date Object +// 4. Value set via `clrDate` input as a Date Object @Directive({ selector: '[clrDate]', @@ -47,28 +55,26 @@ import { DatepickerFocusService } from './providers/datepicker-focus.service'; providers: [DatepickerFocusService], }) export class ClrDateInput extends WrappedFormControl implements OnInit, AfterViewInit, OnDestroy { - protected index = 4; + @Input() placeholder: string; + @Input() clrNewLayout: boolean; + @Output('clrDateChange') dateChange: EventEmitter = new EventEmitter(false); + @Input('clrDate') + set date(date: Date) { + if (this.previousDateChange !== date) { + this.updateDate(this.getValidDateValueFromDate(date)); + } - //We need this variable because if the date input has a value initialized - //we do not output it. This variable is false during initial load. We make sure that - //during initial load dayModelOutputted is equal to the value entered by the user so that initialized - //value isn't emitted back to the user. After initial load, - //we set this to true and the dayModelOutputted is set only - //when the Output is emitted to the user. - private previousOutputInitializedFlag: boolean = false; - private previousOutput: DayModel; - - private initializePreviousOutput(dayModel: DayModel) { - if (!this.previousOutputInitializedFlag) { - this.previousOutput = dayModel; - this.previousOutputInitializedFlag = true; + if (!this.initialClrDateInputValue) { + this.initialClrDateInputValue = date; } } - @Input() clrNewLayout: boolean; + protected index = 4; + private initialClrDateInputValue: Date; + private previousDateChange: Date; constructor( - vcr: ViewContainerRef, + viewContainerRef: ViewContainerRef, injector: Injector, protected el: ElementRef, protected renderer: Renderer2, @@ -76,9 +82,9 @@ export class ClrDateInput extends WrappedFormControl implement @Optional() protected control: NgControl, @Optional() private container: ClrDateContainer, - @Optional() private _dateIOService: DateIOService, - @Optional() private _dateNavigationService: DateNavigationService, - @Optional() private _datepickerEnabledService: DatepickerEnabledService, + @Optional() private dateIOService: DateIOService, + @Optional() private dateNavigationService: DateNavigationService, + @Optional() private datepickerEnabledService: DatepickerEnabledService, @Optional() private dateFormControlService: DateFormControlService, @Inject(PLATFORM_ID) private platformId: Object, @Optional() private focusService: FocusService, @@ -87,268 +93,180 @@ export class ClrDateInput extends WrappedFormControl implement public newFormsLayout: boolean, private datepickerFocusService: DatepickerFocusService ) { - super(vcr, ClrDateContainer, injector, control, renderer, el); + super(viewContainerRef, ClrDateContainer, injector, control, renderer, el); } ngOnInit() { super.ngOnInit(); - this.populateServicesFromContainerComponent(); - this.initializeSubscriptions(); - this.processInitialInputs(); this.setFormLayout(); - } + this.populateServicesFromContainerComponent(); - ngAfterViewInit() { - this.writeInitialInputFromUserInputField(); + this.subscriptions.push( + this.listenForUserSelectedDayChanges(), + this.listenForControlValueChanges(), + this.listenForTouchChanges(), + this.listenForDirtyChanges(), + this.listenForInputRefocus() + ); } - private populateServicesFromContainerComponent(): void { - if (!this.container) { - this._dateIOService = this.getProviderFromContainer(DateIOService); - this._dateNavigationService = this.getProviderFromContainer(DateNavigationService); - this._datepickerEnabledService = this.getProviderFromContainer(DatepickerEnabledService); - this.dateFormControlService = this.getProviderFromContainer(DateFormControlService); - } + ngAfterViewInit() { + // I don't know why I have to do this but after using the new HostWrapping Module I have to delay the processing + // of the initial Input set by the user to here. If I do not 2 issues occur: + // 1. The Input setter is called before ngOnInit. ngOnInit initializes the services without which the setter fails. + // 2. The Renderer doesn't work before ngAfterViewInit (It used to before the new HostWrapping Module for some reason). + // I need the renderer to set the value property on the input to make sure that if the user has supplied a Date + // input object, we reflect it with the right date on the input field using the IO service. I am not sure if + // these are major issues or not but just noting them down here. + this.processInitialInputs(); } - private processInitialInputs(): void { - // Process the inputs initialized by the user which were missed - // because of late subscriptions or lifecycle method calls. - this.processUserDateObject(this.dateValueOnInitialLoad); - - // Handle Initial Value from Reactive Forms - // TODO: We are repeating this logic at multiple places. This makes me think - // if this class should have implemented the ControlValueAccessor interface. - // Will explore that later and see if its a cleaner solution. - if (this.control && this.control.value) { - this.updateInputValue(this.control.value); - this.initializePreviousOutput(this._dateNavigationService.selectedDay); - } + @HostListener('focus') + setFocusStates() { + this.setFocus(true); } - private setFormLayout() { - if (this.clrNewLayout !== undefined) { - this.newFormsLayout = !!this.clrNewLayout; - } + @HostListener('blur') + triggerValidation() { + super.triggerValidation(); + this.setFocus(false); } - private writeInitialInputFromUserInputField() { - // I don't know why I have to do this but after using the new HostWrapping Module I have to delay the processing - // of the initial Input set by the user to here. If I do not 2 issues occur: - // 1. the Input setter is called before ngOnInit. ngOnInit initializes the services without which the setter - // fails - // 2. The Renderer doesn't work before ngAfterViewInit - //(It used to before the new HostWrapping Module for some reason). - // I need the renderer to set the value property on the input to make sure that if the user has supplied a Date - // input object, we reflect it with the right date on the input field using the IO service. I am not sure if - // these are major issues or not but just noting them down here. - if (this._dateNavigationService) { - const selDay: DayModel = this._dateNavigationService.selectedDay; - if (selDay) { - const dateStr: string = this._dateIOService.toLocaleDisplayFormatString(selDay.toDate()); - this.writeDateStrToInputField(dateStr); - } - } - this.initialLoad = false; + @HostBinding('attr.placeholder') + get placeholderText(): string { + return this.placeholder ? this.placeholder : this.dateIOService.placeholderText; } - private writeDateStrToInputField(value: string): void { - this.renderer.setProperty(this.el.nativeElement, 'value', value); + @HostBinding('attr.type') + get inputType(): string { + return isPlatformBrowser(this.platformId) && this.datepickerEnabledService.isEnabled ? 'text' : 'date'; } - private initialLoad: boolean = true; - private dateValueOnInitialLoad: Date; + @HostListener('change', ['$event.target']) + onValueChange(target: HTMLInputElement) { + const validDateValue = this.dateIOService.getDateValueFromDateString(target.value); - /** - * Javascript Date object input set by the user. - */ - @Input('clrDate') - set date(value: Date) { - if (this.initialLoad) { - // Store date value passed by the user to process after the services have been initialized by - // the ngOnInit hook. - this.dateValueOnInitialLoad = value; + if (validDateValue) { + this.updateDate(validDateValue, true); } else { - this.processUserDateObject(value); + this.emitDateOutput(null); + } + } + + private setFocus(focus: boolean) { + if (this.focusService) { + this.focusService.focused = focus; } } - /** - * Processes a date object to check if its valid or not. - */ - private processUserDateObject(value: Date) { - if (this._dateIOService) { - // The date object is converted back to string because in Javascript you can create a date object - // like this: new Date("Test"). This is a date object but it is invalid. Converting the date object - // that the user passed helps us to verify the validity of the date object. - const dateStr: string = this._dateIOService.toLocaleDisplayFormatString(value); - this.updateInputValue(dateStr); + private populateServicesFromContainerComponent() { + if (!this.container) { + this.dateIOService = this.getProviderFromContainer(DateIOService); + this.dateNavigationService = this.getProviderFromContainer(DateNavigationService); + this.datepickerEnabledService = this.getProviderFromContainer(DatepickerEnabledService); + this.dateFormControlService = this.getProviderFromContainer(DateFormControlService); } } - private updateInputValue(dateStr: string): void { - const date: Date = this._dateIOService.isValidInput(dateStr); - if (date) { - const dayModel: DayModel = new DayModel(date.getFullYear(), date.getMonth(), date.getDate()); - if (!dayModel.isEqual(this._dateNavigationService.selectedDay)) { - this.previousOutput = dayModel; - this._dateNavigationService.selectedDay = dayModel; - this.writeDateStrToInputField(dateStr); - } + private processInitialInputs() { + if (this.datepickerHasFormControl()) { + this.updateDate(this.dateIOService.getDateValueFromDateString(this.control.value)); } else { - this._dateNavigationService.selectedDay = null; + this.updateDate(this.initialClrDateInputValue); } } - @Input() placeholder: string; - - /** - * Returns the date format for the placeholder according to which the input should be entered by the user. - */ - @HostBinding('attr.placeholder') - get placeholderText(): string { - return this.placeholder ? this.placeholder : this._dateIOService.placeholderText; + private setFormLayout() { + if (this.clrNewLayout !== undefined) { + this.newFormsLayout = !!this.clrNewLayout; + } } - /** - * Sets the input type to text when the datepicker is enabled. Reverts back to the native date input - * when the datepicker is disabled. Datepicker is disabled on mobiles. - */ - @HostBinding('attr.type') - get inputType(): string { - return isPlatformBrowser(this.platformId) && this._datepickerEnabledService.isEnabled ? 'text' : 'date'; - } + private updateDate(value: Date, setByUserInteraction = false) { + const date = this.getValidDateValueFromDate(value); - /** - * Output Management - * Note: For now we will not emit both clrDateChange and ngControl outputs - * at the same time. This requires us to listen to keydown and blur events to figure out - * exactly when the Output should be emitted. - * Our recommendation right now is to either use clrDate or use ngModel/FormControl. - * Do not use both of them together. - */ - @Output('clrDateChange') _dateUpdated: EventEmitter = new EventEmitter(false); + if (setByUserInteraction) { + this.emitDateOutput(date); + } else { + this.previousDateChange = date; + } - @HostListener('focus') - setFocusStates() { - this.setFocus(true); - } + if (this.dateNavigationService) { + this.dateNavigationService.selectedDay = date + ? new DayModel(date.getFullYear(), date.getMonth(), date.getDate()) + : null; + } - @HostListener('blur') - triggerValidation() { - super.triggerValidation(); - this.setFocus(false); + this.updateInput(date); } - /** - * Fires this method when the user changes the input focuses out of the input field. - */ - @HostListener('change', ['$event.target']) - onValueChange(target: HTMLInputElement) { - const value: string = target.value; - const date: Date = this._dateIOService.isValidInput(value); + private updateInput(date: Date) { if (date) { - const dayModel: DayModel = new DayModel(date.getFullYear(), date.getMonth(), date.getDate()); - this._dateNavigationService.selectedDay = dayModel; - this.emitDateOutput(dayModel); + const dateString = this.dateIOService.toLocaleDisplayFormatString(date); + + if (this.datepickerHasFormControl() && dateString !== this.control.value) { + this.control.control.setValue(dateString); + } else { + this.renderer.setProperty(this.el.nativeElement, 'value', dateString); + } } else { - this._dateNavigationService.selectedDay = null; - this.emitDateOutput(null); + this.renderer.setProperty(this.el.nativeElement, 'value', ''); } } - private emitDateOutput(dayModel: DayModel): void { - if (dayModel && !dayModel.isEqual(this.previousOutput)) { - this._dateUpdated.emit(dayModel.toDate()); - this.previousOutput = dayModel; - } else if (!dayModel && this.previousOutput) { - this._dateUpdated.emit(null); - this.previousOutput = null; + private getValidDateValueFromDate(date: Date) { + if (this.dateIOService) { + const dateString = this.dateIOService.toLocaleDisplayFormatString(date); + return this.dateIOService.getDateValueFromDateString(dateString); + } else { + return null; } } - private setFocus(focus: boolean) { - if (this.focusService) { - this.focusService.focused = focus; + private emitDateOutput(date: Date) { + if (!datesAreEqual(date, this.previousDateChange)) { + this.dateChange.emit(date); + this.previousDateChange = date; + } else if (!date && this.previousDateChange) { + this.dateChange.emit(null); + this.previousDateChange = null; } } - private initializeSubscriptions(): void { - this.listenForUserSelectedDayChanges(); - this.listenForValueChanges(); - this.listenForTouchChanges(); - this.listenForDirtyChanges(); - this.listenForInputRefocus(); + private datepickerHasFormControl() { + return !!this.control; } - private listenForUserSelectedDayChanges() { - if (this._dateNavigationService && this._dateIOService) { - this.subscriptions.push( - this._dateNavigationService.selectedDayChange.subscribe((dayModel: DayModel) => { - const dateStr: string = this._dateIOService.toLocaleDisplayFormatString(dayModel.toDate()); - this.writeDateStrToInputField(dateStr); - // This makes sure that ngModelChange is fired - // TODO: Check if there is a better way to do this. - // NOTE: Its important to use NgControl and not NgModel because - // NgModel only works with template driven forms - if (this.control) { - this.control.control.setValue(dateStr); - } - this.emitDateOutput(dayModel); - }) - ); - } + private listenForControlValueChanges() { + return of(this.datepickerHasFormControl()) + .pipe( + filter(hasControl => hasControl), + switchMap(() => this.control.valueChanges), + // only update date value if not being set by user + filter(() => !this.datepickerFocusService.elementIsFocused(this.el.nativeElement)) + ) + .subscribe((value: string) => this.updateDate(this.dateIOService.getDateValueFromDateString(value))); } - private listenForValueChanges() { - // We do not emit an Output from this subscription because - // we only emit the Output when the user has focused out of the input. - if (this._dateNavigationService && this._dateIOService && this.control) { - this.subscriptions.push( - this.control.valueChanges.subscribe((value: string) => { - const date: Date = this._dateIOService.isValidInput(value); - if (date) { - const dayModel: DayModel = new DayModel(date.getFullYear(), date.getMonth(), date.getDate()); - this._dateNavigationService.selectedDay = dayModel; - this.initializePreviousOutput(dayModel); - } else if (value === '' || value === null) { - this._dateNavigationService.selectedDay = null; - this.initializePreviousOutput(null); - } else { - this.initializePreviousOutput(null); - } - }) - ); - } + private listenForUserSelectedDayChanges() { + return this.dateNavigationService.selectedDayChange.subscribe(dayModel => this.updateDate(dayModel.toDate(), true)); } private listenForTouchChanges() { - if (this.dateFormControlService) { - this.subscriptions.push( - this.dateFormControlService.touchedChange.subscribe(() => { - if (this.control) { - this.control.control.markAsTouched(); - } - }) - ); - } + return this.dateFormControlService.touchedChange + .pipe(filter(() => this.datepickerHasFormControl())) + .subscribe(() => this.control.control.markAsTouched()); } private listenForDirtyChanges() { - this.subscriptions.push( - this.dateFormControlService.dirtyChange.subscribe(() => { - if (this.control) { - this.control.control.markAsDirty(); - } - }) - ); + return this.dateFormControlService.dirtyChange + .pipe(filter(() => this.datepickerHasFormControl())) + .subscribe(() => this.control.control.markAsDirty()); } private listenForInputRefocus() { - this.subscriptions.push( - this._dateNavigationService.selectedDayChange - .pipe(filter(date => !!date)) - .subscribe(v => this.datepickerFocusService.focusInput(this.el.nativeElement)) - ); + return this.dateNavigationService.selectedDayChange + .pipe(filter(date => !!date)) + .subscribe(v => this.datepickerFocusService.focusInput(this.el.nativeElement)); } } diff --git a/src/clr-angular/forms/datepicker/providers/date-io.service.spec.ts b/src/clr-angular/forms/datepicker/providers/date-io.service.spec.ts index 1a2a3753e7..40ad4dc820 100644 --- a/src/clr-angular/forms/datepicker/providers/date-io.service.spec.ts +++ b/src/clr-angular/forms/datepicker/providers/date-io.service.spec.ts @@ -80,112 +80,112 @@ export default function() { it('ignores just text', () => { const inputDate: string = 'abc'; - const date: Date = dateIOService.isValidInput(inputDate); + const date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date).toBeNull(); }); it('ignores invalid dates', () => { let inputDate: string = '10/21/test'; - const date1: Date = dateIOService.isValidInput(inputDate); + const date1: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date1).toBeNull(); inputDate = 'test/1/1'; - const date2: Date = dateIOService.isValidInput(inputDate); + const date2: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date2).toBeNull(); inputDate = 'test test test'; - const date3: Date = dateIOService.isValidInput(inputDate); + const date3: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date3).toBeNull(); }); it('ignores empty strings', () => { const inputDate: string = ''; - const date: Date = dateIOService.isValidInput(inputDate); + const date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date).toBeNull(); }); it('parse a two digit year', () => { let inputDate: string = '01/02/20'; - let date: Date = dateIOService.isValidInput(inputDate); + let date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date).not.toBeNull(); expect(assertEqualDates(date, new Date(2020, 0, 2))).toBe(true); // Invalid date with 2 digit year inputDate = '51/02/20'; - date = dateIOService.isValidInput(inputDate); + date = dateIOService.getDateValueFromDateString(inputDate); expect(date).toBeNull(); }); it('should not parse a five digit year', () => { const inputDate: string = '01/02/10000'; - expect(dateIOService.isValidInput(inputDate)).toBeNull(); + expect(dateIOService.getDateValueFromDateString(inputDate)).toBeNull(); }); it('should not parse a three digit year', () => { const inputDate: string = '01/02/201'; - expect(dateIOService.isValidInput(inputDate)).toBeNull(); + expect(dateIOService.getDateValueFromDateString(inputDate)).toBeNull(); }); it('should not parse a 1 digit year', () => { const inputDate: string = '01/02/2'; - expect(dateIOService.isValidInput(inputDate)).toBeNull(); + expect(dateIOService.getDateValueFromDateString(inputDate)).toBeNull(); }); it('parse a 1 digit date', () => { const inputDate: string = '01/2/2015'; - const date: Date = dateIOService.isValidInput(inputDate); + const date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date).not.toBeNull(); expect(assertEqualDates(date, new Date(2015, 0, 2))).toBe(true); }); it('ignores invalid dates', () => { let inputDate: string = '01/55/2015'; - const date: Date = dateIOService.isValidInput(inputDate); + const date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date).toBeNull(); inputDate = '02/29/2015'; - const date1: Date = dateIOService.isValidInput(inputDate); + const date1: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date1).toBeNull(); // Leap Year inputDate = '02/29/2016'; - const date2: Date = dateIOService.isValidInput(inputDate); + const date2: Date = dateIOService.getDateValueFromDateString(inputDate); expect(assertEqualDates(date2, new Date(2016, 1, 29))).toBe(true); }); it('parses a 1 digit month', () => { const inputDate: string = '1/02/2015'; - const date: Date = dateIOService.isValidInput(inputDate); + const date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(date).not.toBeNull(); expect(assertEqualDates(date, new Date(2015, 0, 2))).toBe(true); }); it('ignores invalid months', () => { const inputDate: string = '13/02/2015'; - expect(dateIOService.isValidInput(inputDate)).toBeNull(); + expect(dateIOService.getDateValueFromDateString(inputDate)).toBeNull(); }); it('ignores the minus sign and considers it as a delimiter', () => { let inputDate: string = '1/-2/2015'; - let date: Date = dateIOService.isValidInput(inputDate); + let date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(assertEqualDates(date, new Date(2015, 0, 2))); inputDate = '-2/5/2015'; - date = dateIOService.isValidInput(inputDate); + date = dateIOService.getDateValueFromDateString(inputDate); expect(assertEqualDates(date, new Date(2015, 1, 5))); inputDate = '1/2/-2015'; - date = dateIOService.isValidInput(inputDate); + date = dateIOService.getDateValueFromDateString(inputDate); expect(assertEqualDates(date, new Date(2015, 0, 2))); }); it('processes dates with different delimiters', () => { let inputDate: string = '1/ 2/2015'; - let date: Date = dateIOService.isValidInput(inputDate); + let date: Date = dateIOService.getDateValueFromDateString(inputDate); expect(assertEqualDates(date, new Date(2015, 0, 2))); inputDate = '1.3 .2016'; - date = dateIOService.isValidInput(inputDate); + date = dateIOService.getDateValueFromDateString(inputDate); expect(assertEqualDates(date, new Date(2016, 0, 3))); }); }); diff --git a/src/clr-angular/forms/datepicker/providers/date-io.service.ts b/src/clr-angular/forms/datepicker/providers/date-io.service.ts index b373de5222..50ffc19892 100644 --- a/src/clr-angular/forms/datepicker/providers/date-io.service.ts +++ b/src/clr-angular/forms/datepicker/providers/date-io.service.ts @@ -133,10 +133,7 @@ export class DateIOService { return result !== -1 ? new Date(result, m, d) : null; } - /** - * Checks if the input provided by the user is valid. - */ - isValidInput(date: string): Date { + getDateValueFromDateString(date: string): Date { if (!date) { return null; } diff --git a/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.spec.ts b/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.spec.ts index 7cd6ef4fb1..fc42858df5 100644 --- a/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.spec.ts +++ b/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.spec.ts @@ -60,11 +60,11 @@ export default function() { const input: HTMLInputElement = fixture.debugElement.nativeElement.querySelector('input'); mockNgZone.stabilizeZone(); - expect(document.activeElement).not.toBe(input); + expect(datepickerFocusService.elementIsFocused(input)).toBe(false); datepickerFocusService.focusInput(input); mockNgZone.stabilizeZone(); - expect(document.activeElement).toBe(input); + expect(datepickerFocusService.elementIsFocused(input)).toBe(true); }) ); }); diff --git a/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.ts b/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.ts index 6fcb010236..3c0b32c765 100644 --- a/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.ts +++ b/src/clr-angular/forms/datepicker/providers/datepicker-focus.service.ts @@ -30,6 +30,10 @@ export class DatepickerFocusService { this._ngZone.runOutsideAngular(() => this.ngZoneIsStableInBrowser().subscribe(() => element.focus())); } + elementIsFocused(element: HTMLInputElement) { + return isPlatformBrowser(this.platformId) && document.activeElement === element; + } + private ngZoneIsStableInBrowser() { // Credit: Material: https://github.com/angular/material2/blob/master/src/lib/datepicker/calendar.ts return this._ngZone.onStable.asObservable().pipe(first(), filter(() => isPlatformBrowser(this.platformId))); diff --git a/src/clr-angular/forms/datepicker/utils/date-utils.spec.ts b/src/clr-angular/forms/datepicker/utils/date-utils.spec.ts new file mode 100644 index 0000000000..c580666c91 --- /dev/null +++ b/src/clr-angular/forms/datepicker/utils/date-utils.spec.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-2019 VMware, Inc. All Rights Reserved. + * This software is released under MIT license. + * The full license information can be found in LICENSE in the root directory of this project. + */ + +import { datesAreEqual, parseToFourDigitYear, getDay, getNumberOfDaysInTheMonth } from './date-utils'; + +describe('date utility functions', () => { + it('should get the number of days in the month', () => { + expect(getNumberOfDaysInTheMonth(2000, 0)).toBe(31); + expect(getNumberOfDaysInTheMonth(2000, 1)).toBe(29); + }); + + it('should take a two digit year and convert to a four digit year', () => { + // window of 80 years before and 20 years after the present year. + expect(parseToFourDigitYear(10)).toBe(2010); + expect(parseToFourDigitYear(1000)).toBe(1000); + expect(parseToFourDigitYear(90)).toBe(1990); + }); + + it('should return the day for the corresponding date where 0 represents Sunday', () => { + expect(getDay(2000, 1, 1)).toBe(2); + expect(getDay(2000, 1, 2)).not.toBe(2); + }); + + it('should determine if two dates are equal', () => { + const date1 = new Date(2000, 1, 1); + const date2 = new Date(2000, 1, 1); + + expect(datesAreEqual(date1, date2)).toBe(true); + expect(datesAreEqual(date2, null)).toBe(false); + }); +}); diff --git a/src/clr-angular/forms/datepicker/utils/date-utils.ts b/src/clr-angular/forms/datepicker/utils/date-utils.ts index 23a926ab7a..acd8b5e538 100644 --- a/src/clr-angular/forms/datepicker/utils/date-utils.ts +++ b/src/clr-angular/forms/datepicker/utils/date-utils.ts @@ -41,3 +41,15 @@ export function parseToFourDigitYear(year: number): number { } return result; } + +export function datesAreEqual(date1: Date, date2: Date) { + if (date1 instanceof Date && date2 instanceof Date) { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ); + } else { + return false; + } +}