diff --git a/lib/core/src/lib/common/utils/date-fns-utils.spec.ts b/lib/core/src/lib/common/utils/date-fns-utils.spec.ts index 058c6bee93..897cc404a1 100644 --- a/lib/core/src/lib/common/utils/date-fns-utils.spec.ts +++ b/lib/core/src/lib/common/utils/date-fns-utils.spec.ts @@ -138,4 +138,37 @@ describe('DateFnsUtils', () => { expect(forceLocalDateJapan.getMonth()).toBe(0); expect(forceLocalDateJapan.getFullYear()).toBe(2020); }); + + it('should detect if a formatted string contains a timezone', () => { + let result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10'); + expect(result).toEqual(false); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00'); + expect(result).toEqual(false); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00Z'); + expect(result).toEqual(true); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00+00:00'); + expect(result).toEqual(true); + + result = DateFnsUtils.stringDateContainsTimeZone('2021-06-09T14:10:00-00:00'); + expect(result).toEqual(true); + }); + + it('should get the date from number', () => { + const spyUtcToLocal = spyOn(DateFnsUtils, 'utcToLocal').and.callThrough(); + + const date = DateFnsUtils.getDate(1623232200000); + expect(date.toISOString()).toBe('2021-06-09T09:50:00.000Z'); + expect(spyUtcToLocal).not.toHaveBeenCalled(); + }); + + it('should get transformed date when string date does not contain the timezone', () => { + const spyUtcToLocal = spyOn(DateFnsUtils, 'utcToLocal').and.callThrough(); + + DateFnsUtils.getDate('2021-06-09T14:10:00'); + + expect(spyUtcToLocal).toHaveBeenCalled(); + }); }); diff --git a/lib/core/src/lib/common/utils/date-fns-utils.ts b/lib/core/src/lib/common/utils/date-fns-utils.ts index 12af9a5e66..7f8daf4f4c 100644 --- a/lib/core/src/lib/common/utils/date-fns-utils.ts +++ b/lib/core/src/lib/common/utils/date-fns-utils.ts @@ -225,4 +225,18 @@ export class DateFnsUtils { const utcDate = `${date.getFullYear()}-${panDate(date.getMonth() + 1)}-${panDate(date.getDate())}T00:00:00.000Z`; return new Date(utcDate); } + + static stringDateContainsTimeZone(value: string): boolean { + return /(Z|([+|-]\d\d:?\d\d))$/.test(value); + } + + static getDate(value: string | number | Date): Date { + let date = new Date(value); + + if (typeof value === 'string' && !DateFnsUtils.stringDateContainsTimeZone(value)) { + date = this.utcToLocal(date); + } + + return date; + } } diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts index 642823ea70..79f0f63de7 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field-validator.ts @@ -159,7 +159,7 @@ export class DateTimeFieldValidator implements FormFieldValidator { } static isValidDateTime(input: string): boolean { - const date = new Date(input); + const date = DateFnsUtils.getDate(input); return isDateValid(date); } @@ -245,19 +245,11 @@ export class MaxDateFieldValidator extends BoundaryDateFieldValidator { } } -/** - * Validates the min constraint for the datetime value. - * - * Notes for developers: - * the format of the min/max values is always the ISO datetime: i.e. 2023-10-01T15:21:00.000Z. - * Min/Max values can be parsed with standard `new Date(value)` calls. - * - */ -export class MinDateTimeFieldValidator implements FormFieldValidator { +export abstract class BoundaryDateTimeFieldValidator implements FormFieldValidator { private supportedTypes = [FormFieldTypes.DATETIME]; isSupported(field: FormFieldModel): boolean { - return field && this.supportedTypes.indexOf(field.type) > -1 && !!field.minValue; + return field && this.supportedTypes.indexOf(field.type) > -1 && !!field[this.getSubjectField()]; } validate(field: FormFieldModel): boolean { @@ -275,16 +267,44 @@ export class MinDateTimeFieldValidator implements FormFieldValidator { private checkDateTime(field: FormFieldModel): boolean { let isValid = true; - const fieldValueDate = new Date(field.value); - const min = new Date(field.minValue); + const fieldValueDate = DateFnsUtils.getDate(field.value); + const subjectFieldDate = DateFnsUtils.getDate(field[this.getSubjectField()]); - if (isBefore(fieldValueDate, min)) { - field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`; - field.validationSummary.attributes.set('minValue', DateFnsUtils.formatDate(DateFnsUtils.utcToLocal(min), field.dateDisplayFormat)); + if (this.compareDates(fieldValueDate, subjectFieldDate)) { + field.validationSummary.message = this.getErrorMessage(); + field.validationSummary.attributes.set(this.getSubjectField(), DateFnsUtils.formatDate(subjectFieldDate, field.dateDisplayFormat)); isValid = false; } return isValid; } + + protected abstract compareDates(fieldValueDate: Date, subjectFieldDate: Date): boolean; + + protected abstract getSubjectField(): string; + + protected abstract getErrorMessage(): string; +} + +/** + * Validates the min constraint for the datetime value. + * + * Notes for developers: + * the format of the min/max values is always the ISO datetime: i.e. 2023-10-01T15:21:00.000Z. + * Min/Max values can be parsed with standard `new Date(value)` calls. + * + */ +export class MinDateTimeFieldValidator extends BoundaryDateTimeFieldValidator { + protected compareDates(fieldValueDate: Date, subjectFieldDate: Date): boolean { + return isBefore(fieldValueDate, subjectFieldDate); + } + + protected getSubjectField(): string { + return 'minValue'; + } + + protected getErrorMessage(): string { + return `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`; + } } /** @@ -295,37 +315,17 @@ export class MinDateTimeFieldValidator implements FormFieldValidator { * Min/Max values can be parsed with standard `new Date(value)` calls. * */ -export class MaxDateTimeFieldValidator implements FormFieldValidator { - private supportedTypes = [FormFieldTypes.DATETIME]; - - isSupported(field: FormFieldModel): boolean { - return field && this.supportedTypes.indexOf(field.type) > -1 && !!field.maxValue; +export class MaxDateTimeFieldValidator extends BoundaryDateTimeFieldValidator { + protected compareDates(fieldValueDate: Date, subjectFieldDate: Date): boolean { + return isAfter(fieldValueDate, subjectFieldDate); } - validate(field: FormFieldModel): boolean { - let isValid = true; - if (this.isSupported(field) && field.value && field.isVisible) { - if (!DateTimeFieldValidator.isValidDateTime(field.value)) { - field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE'; - isValid = false; - } else { - isValid = this.checkDateTime(field); - } - } - return isValid; + protected getSubjectField(): string { + return 'maxValue'; } - private checkDateTime(field: FormFieldModel): boolean { - let isValid = true; - const fieldValueDate = new Date(field.value); - const max = new Date(field.maxValue); - - if (isAfter(fieldValueDate, max)) { - field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`; - field.validationSummary.attributes.set('maxValue', DateFnsUtils.formatDate(DateFnsUtils.utcToLocal(max), field.dateDisplayFormat)); - isValid = false; - } - return isValid; + protected getErrorMessage(): string { + return `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`; } } diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts index 53afd204ef..b0ad4a1f63 100644 --- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts +++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts @@ -461,7 +461,7 @@ export class FormFieldModel extends FormWidgetModel { this.value = new Date(); } - const dateTimeValue = this.value !== null ? new Date(this.value) : null; + const dateTimeValue = this.value !== null ? DateFnsUtils.getDate(this.value) : null; if (isValidDate(dateTimeValue)) { this.form.values[this.id] = dateTimeValue.toISOString(); diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts index 33122e4753..c5e57b933d 100644 --- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts +++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts @@ -77,15 +77,15 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { if (this.field) { if (this.field.minValue) { - this.minDate = DateFnsUtils.utcToLocal(new Date(this.field.minValue)); + this.minDate = DateFnsUtils.getDate(this.field.minValue); } if (this.field.maxValue) { - this.maxDate = DateFnsUtils.utcToLocal(new Date(this.field.maxValue)); + this.maxDate = DateFnsUtils.getDate(this.field.maxValue); } if (this.field.value) { - this.value = new Date(this.field.value); + this.value = DateFnsUtils.getDate(this.field.value); } } } @@ -95,11 +95,12 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { const newValue = this.dateTimeAdapter.parse(input.value, this.field.dateDisplayFormat); if (isValid(newValue)) { - this.field.value = DateFnsUtils.localToUtc(newValue).toISOString(); + this.field.value = newValue.toISOString(); } else { this.field.value = input.value; } + this.value = DateFnsUtils.getDate(this.field.value); this.onFieldChanged(this.field); } @@ -108,7 +109,7 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { const input = event.targetElement as HTMLInputElement; if (newValue && isValid(newValue)) { - this.field.value = DateFnsUtils.localToUtc(newValue).toISOString(); + this.field.value = newValue.toISOString(); } else { this.field.value = input.value; }