AAE-23950 Apply reactive forms for date&time and date widgets (#9852)

* AAE-19102 Date should be formatted correctly on the task form considering specified display format

* validation fix

* rebase

* update validation

* remove duplicated error widgets

* changes after rebase

* unit tests update

* implement suggestions

* test link-adf change

* remove link-adf change

* fix error for provide value with different format than specified

* implement suggestion

* fix timezone issue for min/max dates

* fix another format issue

* exported default date format
This commit is contained in:
Tomasz Gnyp
2024-08-02 18:49:56 +02:00
committed by GitHub
parent d2b0fab677
commit 6d649a8678
21 changed files with 573 additions and 1100 deletions

View File

@@ -20,7 +20,7 @@ import { DateFnsUtils } from './date-fns-utils';
import { Inject, Injectable, Optional } from '@angular/core'; import { Inject, Injectable, Optional } from '@angular/core';
import { MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core'; import { MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core';
import { UserPreferenceValues, UserPreferencesService } from '../services/user-preferences.service'; import { UserPreferenceValues, UserPreferencesService } from '../services/user-preferences.service';
import { Locale } from 'date-fns'; import { isValid, Locale, parse } from 'date-fns';
/** /**
* Date-fns adapter with moment-to-date-fns conversion. * Date-fns adapter with moment-to-date-fns conversion.
@@ -47,15 +47,17 @@ import { Locale } from 'date-fns';
* } * }
*/ */
export const DEFAULT_DATE_FORMAT = 'dd-MM-yyyy';
/** /**
* Material date formats for Date-fns * Material date formats for Date-fns
*/ */
export const ADF_DATE_FORMATS: MatDateFormats = { export const ADF_DATE_FORMATS: MatDateFormats = {
parse: { parse: {
dateInput: 'dd-MM-yyyy' dateInput: DEFAULT_DATE_FORMAT
}, },
display: { display: {
dateInput: 'dd-MM-yyyy', dateInput: DEFAULT_DATE_FORMAT,
monthLabel: 'LLL', monthLabel: 'LLL',
monthYearLabel: 'LLL uuuu', monthYearLabel: 'LLL uuuu',
dateA11yLabel: 'PP', dateA11yLabel: 'PP',
@@ -88,10 +90,11 @@ export class AdfDateFnsAdapter extends DateFnsAdapter {
} }
override parse(value: any, parseFormat: string | string[]): Date { override parse(value: any, parseFormat: string | string[]): Date {
const dateValue = this.isValid(value) ? value : this.parseAndValidateDate(value);
const format = Array.isArray(parseFormat) const format = Array.isArray(parseFormat)
? parseFormat.map(DateFnsUtils.convertMomentToDateFnsFormat) ? parseFormat.map(DateFnsUtils.convertMomentToDateFnsFormat)
: DateFnsUtils.convertMomentToDateFnsFormat(parseFormat); : DateFnsUtils.convertMomentToDateFnsFormat(parseFormat);
return super.parse(value, format); return super.parse(dateValue, format);
} }
override format(date: Date, displayFormat: string): string { override format(date: Date, displayFormat: string): string {
@@ -103,4 +106,9 @@ export class AdfDateFnsAdapter extends DateFnsAdapter {
return super.format(date, displayFormat); return super.format(date, displayFormat);
} }
private parseAndValidateDate(value: any): Date {
const parsedDate = parse(value, this.displayFormat || DEFAULT_DATE_FORMAT, new Date());
return isValid(parsedDate) ? parsedDate : value;
}
} }

View File

@@ -19,7 +19,7 @@ import { Inject, Injectable, Optional } from '@angular/core';
import { DateFnsUtils } from './date-fns-utils'; import { DateFnsUtils } from './date-fns-utils';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimeFormats } from '@mat-datetimepicker/core'; import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimeFormats } from '@mat-datetimepicker/core';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { Locale, addHours, addMinutes } from 'date-fns'; import { Locale, addHours, addMinutes, isValid, parse } from 'date-fns';
/** /**
* Material date/time formats for Date-fns (mat-datetimepicker) * Material date/time formats for Date-fns (mat-datetimepicker)
@@ -126,7 +126,8 @@ export class AdfDateTimeFnsAdapter extends DatetimeAdapter<Date> {
} }
override parse(value: any, parseFormat: any): Date { override parse(value: any, parseFormat: any): Date {
return this._delegate.parse(value, parseFormat); const dateToParse = isValid(new Date(value)) ? parse(value, this.displayFormat, new Date()) : value;
return this._delegate.parse(dateToParse, parseFormat);
} }
override format(date: Date, displayFormat: any): string { override format(date: Date, displayFormat: any): string {

View File

@@ -110,7 +110,7 @@ describe('Form Renderer Component', () => {
expectElementToBeHidden(displayTextElementContainer); expectElementToBeHidden(displayTextElementContainer);
inputDateTestOne.value = '2019-11-19'; inputDateTestOne.value = '2019-11-19';
inputDateTestOne.dispatchEvent(new Event('change')); inputDateTestOne.dispatchEvent(new Event('input'));
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();
@@ -130,7 +130,7 @@ describe('Form Renderer Component', () => {
expectElementToBeVisible(displayTextElementContainer); expectElementToBeVisible(displayTextElementContainer);
inputDateTestOne.value = '2019-11-19'; inputDateTestOne.value = '2019-11-19';
inputDateTestOne.dispatchEvent(new Event('change')); inputDateTestOne.dispatchEvent(new Event('input'));
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable(); await fixture.whenStable();

View File

@@ -1552,7 +1552,7 @@ export const formDateVisibility = {
2: [ 2: [
{ {
id: 'Text0pqd1u', id: 'Text0pqd1u',
name: 'Text', name: 'Text equal specific date',
type: 'text', type: 'text',
readOnly: false, readOnly: false,
required: false, required: false,
@@ -1589,7 +1589,7 @@ export const formDateVisibility = {
1: [ 1: [
{ {
id: 'Text0uyqd3', id: 'Text0uyqd3',
name: 'Text', name: 'Text NOT equal specific date',
type: 'text', type: 'text',
readOnly: false, readOnly: false,
required: false, required: false,

View File

@@ -23,7 +23,7 @@ export class ErrorMessageModel {
constructor(obj?: any) { constructor(obj?: any) {
this.message = obj?.message || ''; this.message = obj?.message || '';
this.attributes = new Map(); this.attributes = obj?.attributes || new Map();
} }
isActive(): boolean { isActive(): boolean {
@@ -31,7 +31,7 @@ export class ErrorMessageModel {
} }
getAttributesAsJsonObj() { getAttributesAsJsonObj() {
let result = {}; const result = {};
if (this.attributes.size > 0) { if (this.attributes.size > 0) {
this.attributes.forEach((value, key) => { this.attributes.forEach((value, key) => {
result[key] = typeof value === 'string' ? value : JSON.stringify(value); result[key] = typeof value === 'string' ? value : JSON.stringify(value);

View File

@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
export class FormFieldTypes { export class FormFieldTypes {
static CONTAINER: string = 'container'; static CONTAINER: string = 'container';
@@ -50,20 +50,13 @@ export class FormFieldTypes {
static DATA_TABLE: string = 'data-table'; static DATA_TABLE: string = 'data-table';
static DISPLAY_EXTERNAL_PROPERTY: string = 'display-external-property'; static DISPLAY_EXTERNAL_PROPERTY: string = 'display-external-property';
static READONLY_TYPES: string[] = [ static READONLY_TYPES: string[] = [FormFieldTypes.HYPERLINK, FormFieldTypes.DISPLAY_VALUE, FormFieldTypes.READONLY_TEXT, FormFieldTypes.GROUP];
FormFieldTypes.HYPERLINK,
FormFieldTypes.DISPLAY_VALUE,
FormFieldTypes.READONLY_TEXT,
FormFieldTypes.GROUP
];
static VALIDATABLE_TYPES: string[] = [ static VALIDATABLE_TYPES: string[] = [FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY];
FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY
];
static CONSTANT_VALUE_TYPES: string[] = [ static REACTIVE_TYPES: string[] = [FormFieldTypes.DATE, FormFieldTypes.DATETIME];
FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY
]; static CONSTANT_VALUE_TYPES: string[] = [FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY];
static isReadOnlyType(type: string) { static isReadOnlyType(type: string) {
return FormFieldTypes.READONLY_TYPES.includes(type); return FormFieldTypes.READONLY_TYPES.includes(type);
@@ -73,6 +66,10 @@ export class FormFieldTypes {
return FormFieldTypes.VALIDATABLE_TYPES.includes(type); return FormFieldTypes.VALIDATABLE_TYPES.includes(type);
} }
static isReactiveType(type: string): boolean {
return FormFieldTypes.REACTIVE_TYPES.includes(type);
}
static isConstantValueType(type: string) { static isConstantValueType(type: string) {
return FormFieldTypes.CONSTANT_VALUE_TYPES.includes(type); return FormFieldTypes.CONSTANT_VALUE_TYPES.includes(type);
} }

View File

@@ -16,7 +16,6 @@
*/ */
import { ErrorMessageModel } from './error-message.model'; import { ErrorMessageModel } from './error-message.model';
import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types'; import { FormFieldTypes } from './form-field-types';
import { import {
FixedValueFieldValidator, FixedValueFieldValidator,
@@ -27,11 +26,6 @@ import {
NumberFieldValidator, NumberFieldValidator,
RegExFieldValidator, RegExFieldValidator,
RequiredFieldValidator, RequiredFieldValidator,
MaxDateTimeFieldValidator,
MinDateTimeFieldValidator,
MaxDateFieldValidator,
MinDateFieldValidator,
DateTimeFieldValidator,
DecimalFieldValidator DecimalFieldValidator
} from './form-field-validator'; } from './form-field-validator';
import { FormFieldModel } from './form-field.model'; import { FormFieldModel } from './form-field.model';
@@ -65,22 +59,6 @@ describe('FormFieldValidator', () => {
expect(validator.validate(field)).toBe(true); expect(validator.validate(field)).toBe(true);
}); });
it('should fail (display error) for dropdown with empty value', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
value: '<empty>',
options: [{ id: 'empty', name: 'Choose option...' }],
hasEmptyValue: true,
required: true
});
field.emptyOption = { id: '<empty>' } as FormFieldOption;
expect(validator.validate(field)).toBe(false);
field.value = '<non-empty>';
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) for multiple type dropdown with zero selection', () => { it('should fail (display error) for multiple type dropdown with zero selection', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN, type: FormFieldTypes.DROPDOWN,
@@ -196,20 +174,6 @@ describe('FormFieldValidator', () => {
expect(validator.validate(field)).toBe(true); expect(validator.validate(field)).toBe(true);
}); });
it('should fail (display error) for date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: null,
required: true
});
field.value = null;
expect(validator.validate(field)).toBe(false);
field.value = '';
expect(validator.validate(field)).toBe(false);
});
it('should succeed for text', () => { it('should succeed for text', () => {
const field = new FormFieldModel(new FormModel(), { const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT, type: FormFieldTypes.TEXT,
@@ -664,466 +628,6 @@ describe('FormFieldValidator', () => {
}); });
}); });
describe('MaxDateTimeFieldValidator', () => {
let validator: MaxDateTimeFieldValidator;
beforeEach(() => {
validator = new MaxDateTimeFieldValidator();
});
it('should require maxValue defined', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME
});
expect(validator.isSupported(field)).toBe(false);
field.maxValue = '9999-02-08 10:10 AM';
expect(validator.isSupported(field)).toBe(true);
});
it('should support date time widgets only', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
maxValue: '9999-02-08 10:10 AM'
});
expect(validator.isSupported(field)).toBe(true);
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBe(false);
});
it('should allow empty values', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: null,
maxValue: '9999-02-08 10:10 AM'
});
expect(validator.validate(field)).toBe(true);
});
it('should succeed for unsupported types', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBe(true);
});
it('should take into account that max value is in UTC and NOT fail (display error) validating value checking the time', () => {
const localValidValue = '2018-03-30T22:59:00.000Z';
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: localValidValue,
maxValue: '2018-03-31T23:00:00.000Z'
});
expect(validator.validate(field)).toBe(true);
});
it('should take into account that max value is in UTC and fail (display error) validating value checking the time', () => {
const localInvalidValue = '2018-03-30T23:01:00.000Z';
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: localInvalidValue,
maxValue: `2018-03-30T23:00:00.000Z`
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN');
});
it('should succeed validating value checking the time', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '9999-02-08T09:10:00.000Z',
maxValue: '9999-02-08T10:10:00.000Z'
});
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) validating value checking the time', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '9999-02-08T11:10:00.000Z',
maxValue: '9999-02-08T10:10:00.000Z'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
it('should succeed validating value checking the date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '9999-02-08T09:10:00.000Z',
maxValue: '9999-02-08T10:10:00.000Z'
});
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) validating value checking the date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '08-02-9999 12:10 AM',
maxValue: '9999-02-07 10:10 AM'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
});
describe('MinDateTimeFieldValidator', () => {
let validator: MinDateTimeFieldValidator;
beforeEach(() => {
validator = new MinDateTimeFieldValidator();
});
it('should require minValue defined', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME
});
expect(validator.isSupported(field)).toBe(false);
field.minValue = '9999-02-08 09:10 AM';
expect(validator.isSupported(field)).toBe(true);
});
it('should support date time widgets only', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
minValue: '9999-02-08 09:10 AM'
});
expect(validator.isSupported(field)).toBe(true);
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBe(false);
});
it('should allow empty values', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: null,
minValue: '9999-02-08 09:10 AM'
});
expect(validator.validate(field)).toBe(true);
});
it('should succeed for unsupported types', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBe(true);
});
it('should take into account that min value is in UTC and NOT fail (display error) validating value checking the time', () => {
const localValidValue = '2018-03-02T06:01:00.000Z';
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: localValidValue,
minValue: '2018-03-02T06:00:00.000Z'
});
expect(validator.validate(field)).toBe(true);
});
it('should take into account that min value is in UTC and fail (display error) validating value checking the time', () => {
const localInvalidValue = '2018-3-02 05:59 AM';
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: localInvalidValue,
minValue: '2018-03-02T06:00:00+00:00'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
});
it('should succeed validating value by time', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '08-02-9999 09:10 AM',
minValue: '9999-02-08 09:00 AM'
});
expect(validator.validate(field)).toBe(true);
});
it('should succeed validating value by date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '09-02-9999 09:10 AM',
minValue: '9999-02-08 09:10 AM'
});
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) validating value by time', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '9999-08-02T08:10:00.000Z',
minValue: '9999-08-02T08:11:00.000Z'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
it('should fail (display error) validating value by date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '9999-02-07T09:10:00.000Z',
minValue: '9999-02-08T09:10:00.000Z'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
});
describe('MaxDateFieldValidator', () => {
let validator: MaxDateFieldValidator;
beforeEach(() => {
validator = new MaxDateFieldValidator();
});
it('should require maxValue defined', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE
});
expect(validator.isSupported(field)).toBe(false);
field.maxValue = '9999-02-08';
expect(validator.isSupported(field)).toBe(true);
});
it('should support date widgets only', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
maxValue: '9999-02-08'
});
expect(validator.isSupported(field)).toBe(true);
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBe(false);
});
it('should allow empty values', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: null,
maxValue: '9999-02-08'
});
expect(validator.validate(field)).toBe(true);
});
it('should succeed for unsupported types', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBe(true);
});
it('should succeed validating value checking the date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
maxValue: '9999-02-09'
});
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) validating value checking the date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
maxValue: '9999-02-07'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
it('should validate with APS1 format', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
maxValue: '09-02-9999'
});
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) validating with APS1 format', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
maxValue: '07-02-9999'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
});
describe('MinDateFieldValidator', () => {
let validator: MinDateFieldValidator;
beforeEach(() => {
validator = new MinDateFieldValidator();
});
it('should require maxValue defined', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE
});
expect(validator.isSupported(field)).toBe(false);
field.minValue = '9999-02-08';
expect(validator.isSupported(field)).toBe(true);
});
it('should support date widgets only', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
minValue: '9999-02-08'
});
expect(validator.isSupported(field)).toBe(true);
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBe(false);
});
it('should allow empty values', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: null,
minValue: '9999-02-08'
});
expect(validator.validate(field)).toBe(true);
});
it('should succeed for unsupported types', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBe(true);
});
it('should succeed validating value checking the date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
minValue: '9999-02-07'
});
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) validating value checking the date', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
minValue: '9999-02-09'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
it('should validate with APS1 format', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
minValue: '07-02-9999'
});
expect(validator.validate(field)).toBe(true);
});
it('should fail (display error) validating with APS1 format', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATE,
value: '9999-02-08T00:00:00',
minValue: '09-02-9999'
});
field.validationSummary = new ErrorMessageModel();
expect(validator.validate(field)).toBe(false);
expect(field.validationSummary).not.toBeNull();
});
});
describe('DateTimeFieldValidator', () => {
let validator: DateTimeFieldValidator;
beforeEach(() => {
validator = new DateTimeFieldValidator();
});
it('should validate dateTime format with dateDisplayFormat', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '2021-06-09 14:10',
dateDisplayFormat: 'YYYY-MM-DD HH:mm'
});
expect(validator.validate(field)).toBe(true);
});
it('should validate dateTime format with default format', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '9-6-2021 11:10 AM'
});
expect(field.value).toBe('9-6-2021 11:10 AM');
expect(field.dateDisplayFormat).toBe('D-M-YYYY hh:mm A');
expect(validator.validate(field)).toBe(true);
});
it('should not validate dateTime format with default format', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DATETIME,
value: '2021-06-09 14:10 AM' // 14:10 does not conform to A
});
expect(field.value).toBe('2021-06-09 14:10 AM');
expect(field.dateDisplayFormat).toBe('D-M-YYYY hh:mm A');
expect(validator.validate(field)).toBe(false);
});
});
describe('DecimalFieldValidator', () => { describe('DecimalFieldValidator', () => {
let decimalValidator: DecimalFieldValidator; let decimalValidator: DecimalFieldValidator;

View File

@@ -20,8 +20,6 @@
import { FormFieldTypes } from './form-field-types'; import { FormFieldTypes } from './form-field-types';
import { isNumberValue } from './form-field-utils'; import { isNumberValue } from './form-field-utils';
import { FormFieldModel } from './form-field.model'; import { FormFieldModel } from './form-field.model';
import { DateFnsUtils } from '../../../../common/utils/date-fns-utils';
import { isValid as isDateValid, isBefore, isAfter } from 'date-fns';
export interface FormFieldValidator { export interface FormFieldValidator {
isSupported(field: FormFieldModel): boolean; isSupported(field: FormFieldModel): boolean;
@@ -42,8 +40,6 @@ export class RequiredFieldValidator implements FormFieldValidator {
FormFieldTypes.UPLOAD, FormFieldTypes.UPLOAD,
FormFieldTypes.AMOUNT, FormFieldTypes.AMOUNT,
FormFieldTypes.DYNAMIC_TABLE, FormFieldTypes.DYNAMIC_TABLE,
FormFieldTypes.DATE,
FormFieldTypes.DATETIME,
FormFieldTypes.ATTACH_FOLDER, FormFieldTypes.ATTACH_FOLDER,
FormFieldTypes.DECIMAL, FormFieldTypes.DECIMAL,
FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY FormFieldTypes.DISPLAY_EXTERNAL_PROPERTY
@@ -127,208 +123,6 @@ export class NumberFieldValidator implements FormFieldValidator {
} }
} }
export class DateFieldValidator implements FormFieldValidator {
private supportedTypes = [FormFieldTypes.DATE];
// Validates that the input string is a valid date formatted as <dateFormat> (default D-M-YYYY)
static isValidDate(inputDate: string, dateFormat: string = 'D-M-YYYY'): boolean {
return DateFnsUtils.isValidDate(inputDate, dateFormat);
}
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value && field.isVisible) {
if (DateFieldValidator.isValidDate(field.value, field.dateDisplayFormat)) {
return true;
}
field.validationSummary.message = field.dateDisplayFormat;
return false;
}
return true;
}
}
export class DateTimeFieldValidator implements FormFieldValidator {
private supportedTypes = [FormFieldTypes.DATETIME];
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1;
}
static isValidDateTime(input: string): boolean {
const date = DateFnsUtils.getDate(input);
return isDateValid(date);
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value && field.isVisible) {
if (DateTimeFieldValidator.isValidDateTime(field.value)) {
return true;
}
field.validationSummary.message = field.dateDisplayFormat;
return false;
}
return true;
}
}
export abstract class BoundaryDateFieldValidator implements FormFieldValidator {
DATE_FORMAT_CLOUD = 'YYYY-MM-DD';
DATE_FORMAT = 'DD-MM-YYYY';
supportedTypes = [FormFieldTypes.DATE];
validate(field: FormFieldModel): boolean {
let isValid = true;
if (this.isSupported(field) && field.value && field.isVisible) {
const dateFormat = field.dateDisplayFormat;
if (!DateFieldValidator.isValidDate(field.value, dateFormat)) {
field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_DATE';
isValid = false;
} else {
isValid = this.checkDate(field, dateFormat);
}
}
return isValid;
}
extractDateFormat(date: string): string {
const brokenDownDate = date.split('-');
return brokenDownDate[0].length === 4 ? this.DATE_FORMAT_CLOUD : this.DATE_FORMAT;
}
abstract checkDate(field: FormFieldModel, dateFormat: string);
abstract isSupported(field: FormFieldModel);
}
export class MinDateFieldValidator extends BoundaryDateFieldValidator {
checkDate(field: FormFieldModel, dateFormat: string): boolean {
let isValid = true;
const fieldValueData = DateFnsUtils.parseDate(field.value, dateFormat, { dateOnly: true });
const minValueDateFormat = this.extractDateFormat(field.minValue);
const min = DateFnsUtils.parseDate(field.minValue, minValueDateFormat);
if (DateFnsUtils.isBeforeDate(fieldValueData, min)) {
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_LESS_THAN`;
field.validationSummary.attributes.set('minValue', DateFnsUtils.formatDate(min, field.dateDisplayFormat).toLocaleUpperCase());
isValid = false;
}
return isValid;
}
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1 && !!field.minValue;
}
}
export class MaxDateFieldValidator extends BoundaryDateFieldValidator {
checkDate(field: FormFieldModel, dateFormat: string): boolean {
let isValid = true;
const fieldValueData = DateFnsUtils.parseDate(field.value, dateFormat, { dateOnly: true });
const maxValueDateFormat = this.extractDateFormat(field.maxValue);
const max = DateFnsUtils.parseDate(field.maxValue, maxValueDateFormat);
if (DateFnsUtils.isAfterDate(fieldValueData, max)) {
field.validationSummary.message = `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`;
field.validationSummary.attributes.set('maxValue', DateFnsUtils.formatDate(max, field.dateDisplayFormat).toLocaleUpperCase());
isValid = false;
}
return isValid;
}
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1 && !!field.maxValue;
}
}
export abstract class BoundaryDateTimeFieldValidator implements FormFieldValidator {
private supportedTypes = [FormFieldTypes.DATETIME];
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1 && !!field[this.getSubjectField()];
}
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;
}
private checkDateTime(field: FormFieldModel): boolean {
let isValid = true;
const fieldValueDate = DateFnsUtils.getDate(field.value);
const subjectFieldDate = DateFnsUtils.getDate(field[this.getSubjectField()]);
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`;
}
}
/**
* Validates the max 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 MaxDateTimeFieldValidator extends BoundaryDateTimeFieldValidator {
protected compareDates(fieldValueDate: Date, subjectFieldDate: Date): boolean {
return isAfter(fieldValueDate, subjectFieldDate);
}
protected getSubjectField(): string {
return 'maxValue';
}
protected getErrorMessage(): string {
return `FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`;
}
}
export class MinLengthFieldValidator implements FormFieldValidator { export class MinLengthFieldValidator implements FormFieldValidator {
private supportedTypes = [FormFieldTypes.TEXT, FormFieldTypes.MULTILINE_TEXT]; private supportedTypes = [FormFieldTypes.TEXT, FormFieldTypes.MULTILINE_TEXT];
@@ -523,12 +317,6 @@ export const FORM_FIELD_VALIDATORS = [
new MinValueFieldValidator(), new MinValueFieldValidator(),
new MaxValueFieldValidator(), new MaxValueFieldValidator(),
new RegExFieldValidator(), new RegExFieldValidator(),
new DateFieldValidator(),
new DateTimeFieldValidator(),
new MinDateFieldValidator(),
new MaxDateFieldValidator(),
new FixedValueFieldValidator(), new FixedValueFieldValidator(),
new MinDateTimeFieldValidator(),
new MaxDateTimeFieldValidator(),
new DecimalFieldValidator() new DecimalFieldValidator()
]; ];

View File

@@ -143,6 +143,10 @@ export class FormFieldModel extends FormWidgetModel {
this._isValid = false; this._isValid = false;
} }
markAsValid() {
this._isValid = true;
}
validate(): boolean { validate(): boolean {
this.validationSummary = new ErrorMessageModel(); this.validationSummary = new ErrorMessageModel();

View File

@@ -148,13 +148,13 @@ export class FormModel implements ProcessFormModel {
validateForm(): void { validateForm(): void {
const validateFormEvent: any = new ValidateFormEvent(this); const validateFormEvent: any = new ValidateFormEvent(this);
const errorsField: FormFieldModel[] = []; const errorsField: FormFieldModel[] = this.fieldsCache.filter((field) => {
if (!FormFieldTypes.isReactiveType(field.type)) {
for (let i = 0; i < this.fieldsCache.length; i++) { return !field.validate();
if (!this.fieldsCache[i].validate()) { } else {
errorsField.push(this.fieldsCache[i]); return field.validationSummary.isActive();
} }
} });
this.isValid = errorsField.length <= 0; this.isValid = errorsField.length <= 0;
@@ -191,7 +191,7 @@ export class FormModel implements ProcessFormModel {
return; return;
} }
if (!field.validate()) { if (!FormFieldTypes.isReactiveType(field.type) && !field.validate()) {
this.markAsInvalid(); this.markAsInvalid();
} }

View File

@@ -1,6 +1,6 @@
<div class="{{ field.className }}" <div class="{{ field.className }}"
id="data-time-widget" id="data-time-widget"
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-invalid]="datetimeInputControl.invalid && datetimeInputControl.touched"
[class.adf-left-label-input-container]="field.leftLabels"> [class.adf-left-label-input-container]="field.leftLabels">
<div *ngIf="field.leftLabels"> <div *ngIf="field.leftLabels">
<label class="adf-label adf-left-label" [attr.for]="field.id"> <label class="adf-label adf-left-label" [attr.for]="field.id">
@@ -17,29 +17,24 @@
<input matInput <input matInput
[matDatetimepicker]="datetimePicker" [matDatetimepicker]="datetimePicker"
[id]="field.id" [id]="field.id"
[(ngModel)]="value" [formControl]="datetimeInputControl"
[required]="isRequired()"
[disabled]="field.readOnly"
(change)="onValueChanged($event)"
(dateChange)="onDateChanged($event)"
(keydown.enter)="datetimePicker.open()" (keydown.enter)="datetimePicker.open()"
[placeholder]="field.placeholder" [placeholder]="field.placeholder"
[title]="field.tooltip" [title]="field.tooltip"
(blur)="markAsTouched()" (blur)="updateField()"
[min]="minDate" [min]="minDate"
[max]="maxDate"> [max]="maxDate">
<mat-datetimepicker-toggle matSuffix [for]="datetimePicker" <mat-datetimepicker-toggle matSuffix [for]="datetimePicker"
[disabled]="field.readOnly"></mat-datetimepicker-toggle> [disabled]="field.readOnly">
</mat-datetimepicker-toggle>
<mat-datetimepicker #datetimePicker
data-automation-id="adf-date-time-widget-picker"
type="datetime"
[touchUi]="true"
[timeInterval]="5"
[disabled]="field.readOnly">
</mat-datetimepicker>
</mat-form-field> </mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget> <error-widget *ngIf="datetimeInputControl.invalid && datetimeInputControl.touched" [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datetimepicker #datetimePicker
data-automation-id="adf-date-time-widget-picker"
type="datetime"
[touchUi]="true"
[timeInterval]="5"
[disabled]="field.readOnly">
</mat-datetimepicker>
</div> </div>
</div> </div>

View File

@@ -22,12 +22,10 @@ import { DateTimeWidgetComponent } from './date-time.widget';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { FormFieldTypes } from '../core/form-field-types'; import { FormFieldTypes } from '../core/form-field-types';
import { DateFieldValidator, DateTimeFieldValidator } from '../core';
import { HarnessLoader } from '@angular/cdk/testing'; import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatInputHarness } from '@angular/material/input/testing'; import { MatInputHarness } from '@angular/material/input/testing';
import { addMinutes } from 'date-fns'; import { addMinutes } from 'date-fns';
import { HttpClientModule } from '@angular/common/http';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
@@ -35,6 +33,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('DateTimeWidgetComponent', () => { describe('DateTimeWidgetComponent', () => {
let loader: HarnessLoader; let loader: HarnessLoader;
@@ -47,7 +46,7 @@ describe('DateTimeWidgetComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot(), TranslateModule.forRoot(),
HttpClientModule, HttpClientTestingModule,
NoopAnimationsModule, NoopAnimationsModule,
MatDialogModule, MatDialogModule,
MatMenuModule, MatMenuModule,
@@ -65,7 +64,6 @@ describe('DateTimeWidgetComponent', () => {
widget = fixture.componentInstance; widget = fixture.componentInstance;
form = new FormModel(); form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new DateTimeFieldValidator()];
loader = TestbedHarnessEnvironment.loader(fixture); loader = TestbedHarnessEnvironment.loader(fixture);
}); });
@@ -74,17 +72,16 @@ describe('DateTimeWidgetComponent', () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
}); });
it('should setup min value for date picker', async () => { it('should setup min value for date picker', () => {
const minValue = '1982-03-13T10:00:00Z'; const minValue = '1982-03-13T10:00:00Z';
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
type: 'datetime', type: FormFieldTypes.DATETIME,
minValue minValue
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(widget.minDate.toISOString()).toBe(`1982-03-13T10:00:00.000Z`); expect(widget.minDate.toISOString()).toBe(`1982-03-13T10:00:00.000Z`);
}); });
@@ -93,7 +90,7 @@ describe('DateTimeWidgetComponent', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
fixture.detectChanges(); fixture.detectChanges();
@@ -101,13 +98,12 @@ describe('DateTimeWidgetComponent', () => {
expect(element.querySelector('#data-time-widget')).not.toBeNull(); expect(element.querySelector('#data-time-widget')).not.toBeNull();
}); });
it('should setup max value for date picker', async () => { it('should setup max value for date picker', () => {
const maxValue = '1982-03-13T10:00:00Z'; const maxValue = '1982-03-13T10:00:00Z';
widget.field = new FormFieldModel(null, { widget.field = new FormFieldModel(null, {
maxValue maxValue
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(widget.maxDate.toISOString()).toBe('1982-03-13T10:00:00.000Z'); expect(widget.maxDate.toISOString()).toBe('1982-03-13T10:00:00.000Z');
}); });
@@ -119,76 +115,70 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-09-12T09:00:00.000Z', value: '9999-09-12T09:00:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
widget.field = field; widget.field = field;
widget.onDateChanged({ value: new Date('1982-03-13T10:00:00.000Z') } as any);
fixture.detectChanges();
widget.datetimeInputControl.setValue(new Date('1982-03-13T10:00:00.000Z'));
expect(widget.onFieldChanged).toHaveBeenCalledWith(field); expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
}); });
it('should validate the initial datetime value', async () => { it('should validate the initial datetime value', () => {
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-09-12T09:00:00.000Z', value: '9999-09-12T09:00:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
widget.field = field; widget.field = field;
fixture.whenStable(); fixture.detectChanges();
await fixture.whenStable();
expect(field.isValid).toBeTrue(); expect(field.isValid).toBeTrue();
}); });
it('should validate the updated datetime value', async () => { it('should validate the updated datetime value', () => {
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-09-12T09:00:00.000Z', value: '9999-09-12T09:00:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
widget.field = field; widget.field = field;
fixture.whenStable(); fixture.detectChanges();
await fixture.whenStable();
let expectedDate = new Date('9999-09-12T09:10:00.000Z'); let expectedDate = new Date('9999-09-12T09:10:00.000Z');
expectedDate = addMinutes(expectedDate, expectedDate.getTimezoneOffset()); expectedDate = addMinutes(expectedDate, expectedDate.getTimezoneOffset());
widget.onDateChanged({ value: expectedDate } as any); widget.datetimeInputControl.setValue(expectedDate);
expect(field.value).toBe('9999-09-12T09:10:00.000Z'); expect(field.value).toEqual(new Date('9999-09-12T09:10:00.000Z'));
expect(field.isValid).toBeTrue(); expect(field.isValid).toBeTrue();
}); });
it('should forwad the incorrect datetime input for further validation', async () => { it('should forwad the incorrect datetime input for further validation', () => {
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-09-12T09:00:00.000Z', value: '9999-09-12T09:00:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
widget.field = field; widget.field = field;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
widget.onDateChanged({ widget.datetimeInputControl.setValue(new Date('123abc'));
value: null,
targetElement: {
value: '123abc'
}
} as any);
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(field.value).toBe('123abc'); expect(widget.datetimeInputControl.invalid).toBeTrue();
expect(field.isValid).toBeFalse(); expect(field.isValid).toBeFalse();
expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A'); expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A');
}); });
@@ -198,7 +188,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-09-12T09:00:00.000Z', value: '9999-09-12T09:00:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
widget.field = field; widget.field = field;
@@ -206,35 +196,31 @@ describe('DateTimeWidgetComponent', () => {
fixture.whenStable(); fixture.whenStable();
await fixture.whenStable(); await fixture.whenStable();
widget.onValueChanged({ target: { value: '9999-09-12T09:10:00.000Z' } } as any); const input = await loader.getHarness(MatInputHarness);
await input.setValue('9999-09-12T09:10:00.000Z');
expect(field.value).toBe('9999-09-12T09:10:00.000Z'); expect(field.value).toEqual(new Date('9999-09-12T09:10:00.000Z'));
expect(field.isValid).toBeTrue(); expect(field.isValid).toBeTrue();
}); });
it('should fail validating incorrect keyboard input', async () => { it('should fail validating incorrect keyboard input', () => {
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-09-12T09:00:00.000Z', value: '9999-09-12T09:00:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
widget.field = field; widget.field = field;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
widget.onValueChanged({ const dateTimeInput = fixture.nativeElement.querySelector('input');
target: { dateTimeInput.value = '123abc';
value: '123abc' dateTimeInput.dispatchEvent(new Event('input'));
}
} as any);
fixture.detectChanges(); expect(widget.datetimeInputControl.invalid).toBeTrue();
await fixture.whenStable(); expect(field.value).toBe(null);
expect(field.value).toBe('123abc');
expect(field.isValid).toBeFalse(); expect(field.isValid).toBeFalse();
expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A'); expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A');
}); });
@@ -244,7 +230,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-09-12T09:00:00.000Z', value: '9999-09-12T09:00:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
widget.field = field; widget.field = field;
@@ -252,9 +238,12 @@ describe('DateTimeWidgetComponent', () => {
fixture.whenStable(); fixture.whenStable();
await fixture.whenStable(); await fixture.whenStable();
widget.onDateChanged({ value: null, targetElement: { value: '' } } as any); const input = await loader.getHarness(MatInputHarness);
await input.setValue(null);
expect(field.value).toBe(''); expect(widget.datetimeInputControl.value).toBe(null);
expect(widget.datetimeInputControl.valid).toBeTrue();
expect(field.value).toBe(null);
expect(field.isValid).toBeTrue(); expect(field.isValid).toBeTrue();
}); });
@@ -282,6 +271,7 @@ describe('DateTimeWidgetComponent', () => {
type: FormFieldTypes.DATETIME, type: FormFieldTypes.DATETIME,
required: true required: true
}); });
fixture.detectChanges();
}); });
it('should be marked as invalid after interaction', () => { it('should be marked as invalid after interaction', () => {
@@ -296,8 +286,6 @@ describe('DateTimeWidgetComponent', () => {
}); });
it('should be able to display label with asterisk', () => { it('should be able to display label with asterisk', () => {
fixture.detectChanges();
const asterisk = element.querySelector<HTMLElement>('.adf-asterisk'); const asterisk = element.querySelector<HTMLElement>('.adf-asterisk');
expect(asterisk).not.toBeNull(); expect(asterisk).not.toBeNull();
@@ -311,7 +299,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-11-30T10:30:00.000Z', value: '9999-11-30T10:30:00.000Z',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
fixture.detectChanges(); fixture.detectChanges();
@@ -327,7 +315,7 @@ describe('DateTimeWidgetComponent', () => {
name: 'date-name', name: 'date-name',
value: '9999-12-30T10:30:00.000Z', value: '9999-12-30T10:30:00.000Z',
dateDisplayFormat: 'MM-DD-YYYY HH:mm A', dateDisplayFormat: 'MM-DD-YYYY HH:mm A',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
fixture.detectChanges(); fixture.detectChanges();
@@ -343,7 +331,7 @@ describe('DateTimeWidgetComponent', () => {
name: 'date-name', name: 'date-name',
value: '9999-12-30T10:30:00.000Z', value: '9999-12-30T10:30:00.000Z',
dateDisplayFormat: 'MM-DD-YYYY HH:mm A', dateDisplayFormat: 'MM-DD-YYYY HH:mm A',
type: 'datetime' type: FormFieldTypes.DATETIME
}); });
fixture.detectChanges(); fixture.detectChanges();
@@ -365,7 +353,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'datetime-field-name', name: 'datetime-field-name',
value: '9999-12-30T10:30:00.000Z', value: '9999-12-30T10:30:00.000Z',
type: 'datetime', type: FormFieldTypes.DATETIME,
dateDisplayFormat: 'MM-DD-YYYY HH:mm A' dateDisplayFormat: 'MM-DD-YYYY HH:mm A'
}); });
widget.field = field; widget.field = field;
@@ -385,6 +373,31 @@ describe('DateTimeWidgetComponent', () => {
expect(await input.getValue()).toBe('03-02-2020 00:00 AM'); expect(await input.getValue()).toBe('03-02-2020 00:00 AM');
}); });
it('should display value with specified format when format of provided datetime is different', async () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'datetime-field-name',
value: '9999-12-30T10:30:00.000Z',
type: FormFieldTypes.DATETIME,
dateDisplayFormat: 'MM/DD/YYYY HH;mm A'
});
widget.field = field;
fixture.detectChanges();
await fixture.whenStable();
const input = await loader.getHarness(MatInputHarness);
expect(await input.getValue()).toBe('12/30/9999 10;30 AM');
widget.field.value = '2020-03-02T00:00:00.000Z';
fixture.componentInstance.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
expect(await input.getValue()).toBe('03/02/2020 00;00 AM');
});
describe('when form model has left labels', () => { describe('when form model has left labels', () => {
it('should have left labels classes on leftLabels true', () => { it('should have left labels classes on leftLabels true', () => {
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), { widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {

View File

@@ -18,18 +18,20 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { NgIf } from '@angular/common'; import { NgIf } from '@angular/common';
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerInputEvent, MatDatetimepickerModule } from '@mat-datetimepicker/core'; import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerModule } from '@mat-datetimepicker/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { isValid } from 'date-fns';
import { ADF_DATE_FORMATS, ADF_DATETIME_FORMATS, AdfDateFnsAdapter, AdfDateTimeFnsAdapter, DateFnsUtils } from '../../../../common'; import { ADF_DATE_FORMATS, ADF_DATETIME_FORMATS, AdfDateFnsAdapter, AdfDateTimeFnsAdapter, DateFnsUtils } from '../../../../common';
import { FormService } from '../../../services/form.service'; import { FormService } from '../../../services/form.service';
import { ErrorWidgetComponent } from '../error/error.component'; import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component'; import { WidgetComponent } from '../widget.component';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ErrorMessageModel } from '../core/error-message.model';
@Component({ @Component({
selector: 'date-time-widget', selector: 'date-time-widget',
@@ -42,68 +44,116 @@ import { WidgetComponent } from '../widget.component';
], ],
templateUrl: './date-time.widget.html', templateUrl: './date-time.widget.html',
styleUrls: ['./date-time.widget.scss'], styleUrls: ['./date-time.widget.scss'],
imports: [NgIf, TranslateModule, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, FormsModule, ErrorWidgetComponent], imports: [NgIf, TranslateModule, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, ReactiveFormsModule, ErrorWidgetComponent],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { export class DateTimeWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
minDate: Date; minDate: Date;
maxDate: Date; maxDate: Date;
datetimeInputControl: FormControl<Date> = new FormControl<Date>(null);
@Input() private onDestroy$ = new Subject<void>();
value: any = null;
constructor(public formService: FormService, private dateAdapter: DateAdapter<Date>, private dateTimeAdapter: DatetimeAdapter<Date>) { public readonly formService = inject(FormService);
super(formService); private readonly dateAdapter = inject(DateAdapter);
private readonly dateTimeAdapter = inject(DatetimeAdapter);
ngOnInit(): void {
this.patchFormControl();
this.initDateAdapter();
this.initDateRange();
this.subscribeToDateChanges();
this.updateField();
} }
ngOnInit() { updateField(): void {
if (this.field.dateDisplayFormat) { this.validateField();
this.onFieldChanged(this.field);
}
private patchFormControl(): void {
this.datetimeInputControl.setValue(this.field.value, { emitEvent: false });
this.datetimeInputControl.setValidators(this.isRequired() ? [Validators.required] : []);
if (this.field?.readOnly || this.readOnly) {
this.datetimeInputControl.disable({ emitEvent: false });
}
this.datetimeInputControl.updateValueAndValidity({ emitEvent: false });
}
private subscribeToDateChanges(): void {
this.datetimeInputControl.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe((newDate: Date) => {
this.field.value = newDate;
this.updateField();
});
}
private validateField(): void {
if (this.datetimeInputControl?.invalid) {
this.handleErrors(this.datetimeInputControl.errors);
this.field.markAsInvalid();
} else {
this.resetErrors();
this.field.markAsValid();
}
}
private handleErrors(errors: ValidationErrors): void {
const errorAttributes = new Map<string, string>();
switch (true) {
case !!errors.matDatepickerParse:
this.updateValidationSummary(this.field.dateDisplayFormat || this.field.defaultDateTimeFormat);
break;
case !!errors.required:
this.updateValidationSummary('FORM.FIELD.REQUIRED');
break;
case !!errors.matDatepickerMin: {
const minValue = DateFnsUtils.formatDate(errors.matDatepickerMin.min, this.field.dateDisplayFormat).toLocaleUpperCase();
errorAttributes.set('minValue', minValue);
this.updateValidationSummary('FORM.FIELD.VALIDATOR.NOT_LESS_THAN', errorAttributes);
break;
}
case !!errors.matDatepickerMax: {
const maxValue = DateFnsUtils.formatDate(errors.matDatepickerMax.max, this.field.dateDisplayFormat).toLocaleUpperCase();
errorAttributes.set('maxValue', maxValue);
this.updateValidationSummary('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN', errorAttributes);
break;
}
default:
break;
}
}
private updateValidationSummary(message: string, attributes?: Map<string, string>): void {
this.field.validationSummary = new ErrorMessageModel({ message, attributes });
}
private resetErrors(): void {
this.updateValidationSummary('');
}
private initDateAdapter(): void {
if (this.field?.dateDisplayFormat) {
const dateAdapter = this.dateAdapter as AdfDateFnsAdapter; const dateAdapter = this.dateAdapter as AdfDateFnsAdapter;
dateAdapter.displayFormat = this.field.dateDisplayFormat; dateAdapter.displayFormat = this.field.dateDisplayFormat;
const dateTimeAdapter = this.dateTimeAdapter as AdfDateTimeFnsAdapter; const dateTimeAdapter = this.dateTimeAdapter as AdfDateTimeFnsAdapter;
dateTimeAdapter.displayFormat = this.field.dateDisplayFormat; dateTimeAdapter.displayFormat = this.field.dateDisplayFormat;
} }
}
if (this.field) { private initDateRange(): void {
if (this.field.minValue) { if (this.field?.minValue) {
this.minDate = DateFnsUtils.getDate(this.field.minValue); this.minDate = DateFnsUtils.getDate(this.field.minValue);
} }
if (this.field.maxValue) { if (this.field?.maxValue) {
this.maxDate = DateFnsUtils.getDate(this.field.maxValue); this.maxDate = DateFnsUtils.getDate(this.field.maxValue);
}
if (this.field.value) {
this.value = DateFnsUtils.getDate(this.field.value);
}
} }
} }
onValueChanged(event: Event) { ngOnDestroy(): void {
const input = event.target as HTMLInputElement; this.onDestroy$.next();
const newValue = this.dateTimeAdapter.parse(input.value, this.field.dateDisplayFormat); this.onDestroy$.complete();
if (isValid(newValue)) {
this.field.value = newValue.toISOString();
} else {
this.field.value = input.value;
}
this.value = DateFnsUtils.getDate(this.field.value);
this.onFieldChanged(this.field);
}
onDateChanged(event: MatDatetimepickerInputEvent<Date>) {
const newValue = event.value;
const input = event.targetElement as HTMLInputElement;
if (newValue && isValid(newValue)) {
this.field.value = newValue.toISOString();
} else {
this.field.value = input.value;
}
this.onFieldChanged(this.field);
} }
} }

View File

@@ -1,4 +1,4 @@
<div class="{{ field.className }}" id="data-widget" [class.adf-invalid]="!field.isValid && isTouched()"> <div class="{{ field.className }}" id="data-widget" [class.adf-invalid]="dateInputControl.invalid && dateInputControl.touched">
<mat-form-field [floatLabel]="'always'" class="adf-date-widget"> <mat-form-field [floatLabel]="'always'" class="adf-date-widget">
<mat-label class="adf-label" <mat-label class="adf-label"
[id]="field.id + '-label'" [id]="field.id + '-label'"
@@ -8,21 +8,16 @@
<input matInput <input matInput
[matDatepicker]="datePicker" [matDatepicker]="datePicker"
[id]="field.id" [id]="field.id"
[(ngModel)]="value" [formControl]="dateInputControl"
[required]="field.required"
[placeholder]="field.placeholder" [placeholder]="field.placeholder"
[min]="minDate" [min]="minDate"
[max]="maxDate" [max]="maxDate"
[disabled]="field.readOnly" (blur)="updateField()">
(dateChange)="onDateChange($event)"
(blur)="markAsTouched()">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly"></mat-datepicker-toggle>
<mat-datepicker #datePicker <mat-datepicker #datePicker
[startAt]="startAt" [startAt]="startAt"
[disabled]="field.readOnly"> [disabled]="field.readOnly">
</mat-datepicker> </mat-datepicker>
</mat-form-field> </mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget> <error-widget *ngIf="dateInputControl.invalid && dateInputControl.touched" [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div> </div>

View File

@@ -18,8 +18,10 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DateAdapter } from '@angular/material/core'; import { DateAdapter } from '@angular/material/core';
import { CoreTestingModule } from '../../../../testing'; import { CoreTestingModule } from '../../../../testing';
import { DateFieldValidator, FormFieldModel, FormFieldTypes, FormModel, MaxDateFieldValidator, MinDateFieldValidator } from '../core'; import { FormFieldModel, FormFieldTypes, FormModel } from '../core';
import { DateWidgetComponent } from './date.widget'; import { DateWidgetComponent } from './date.widget';
import { DEFAULT_DATE_FORMAT } from '../../../../common';
import { isEqual } from 'date-fns';
describe('DateWidgetComponent', () => { describe('DateWidgetComponent', () => {
let widget: DateWidgetComponent; let widget: DateWidgetComponent;
@@ -34,7 +36,6 @@ describe('DateWidgetComponent', () => {
}); });
form = new FormModel(); form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new MinDateFieldValidator(), new MaxDateFieldValidator()];
fixture = TestBed.createComponent(DateWidgetComponent); fixture = TestBed.createComponent(DateWidgetComponent);
adapter = fixture.debugElement.injector.get(DateAdapter); adapter = fixture.debugElement.injector.get(DateAdapter);
@@ -54,8 +55,8 @@ describe('DateWidgetComponent', () => {
}); });
it('should setup min value for date picker', () => { it('should setup min value for date picker', () => {
const minValue = '13-03-1982'; const minValue = '1982-03-13';
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(null, {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
minValue minValue
@@ -63,12 +64,12 @@ describe('DateWidgetComponent', () => {
widget.ngOnInit(); widget.ngOnInit();
const expected = adapter.parse(minValue, widget.DATE_FORMAT) as Date; const expected = adapter.parse(minValue, DEFAULT_DATE_FORMAT);
expect(adapter.compareDate(widget.minDate, expected)).toBe(0); expect(isEqual(widget.minDate, expected)).toBeTrue();
}); });
it('should validate min date value constraint', async () => { it('should validate min date value constraint', () => {
const minValue = '13-03-1982'; const minValue = '1982-03-13';
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-id', id: 'date-id',
@@ -79,22 +80,19 @@ describe('DateWidgetComponent', () => {
}); });
widget.field = field; widget.field = field;
widget.ngOnInit(); fixture.detectChanges();
widget.onDateChange({ widget.dateInputControl.setValue(new Date('1982/03/12'));
value: new Date('1982/03/12')
} as any);
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(widget.field.isValid).toBeFalsy(); expect(widget.field.isValid).toBeFalsy();
expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN'); expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
expect(field.validationSummary.attributes.get('minValue')).toBe('13-03-1982'); expect(field.validationSummary.attributes.get('minValue')).toBe('13-03-1982');
}); });
it('should validate max date value constraint', async () => { it('should validate max date value constraint', () => {
const maxValue = '13-03-1982'; const maxValue = '1982-03-13';
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-id', id: 'date-id',
@@ -105,14 +103,11 @@ describe('DateWidgetComponent', () => {
}); });
widget.field = field; widget.field = field;
widget.ngOnInit(); fixture.detectChanges();
widget.onDateChange({ widget.dateInputControl.setValue(new Date('2023/03/13'));
value: new Date('2023/03/13')
} as any);
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(widget.field.isValid).toBeFalsy(); expect(widget.field.isValid).toBeFalsy();
expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN'); expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN');
@@ -132,13 +127,13 @@ describe('DateWidgetComponent', () => {
}); });
it('should setup max value for date picker', () => { it('should setup max value for date picker', () => {
const maxValue = '31-03-1982'; const maxValue = '1982-03-31';
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
maxValue maxValue
}); });
widget.ngOnInit(); fixture.detectChanges();
const expected = adapter.parse(maxValue, widget.DATE_FORMAT) as Date; const expected = adapter.parse(maxValue, DEFAULT_DATE_FORMAT) as Date;
expect(adapter.compareDate(widget.maxDate, expected)).toBe(0); expect(adapter.compareDate(widget.maxDate, expected)).toBe(0);
}); });
@@ -153,9 +148,10 @@ describe('DateWidgetComponent', () => {
}); });
widget.field = field; widget.field = field;
widget.onDateChange({
value: new Date('12/12/2012') fixture.detectChanges();
} as any);
widget.dateInputControl.setValue(new Date('12/12/2012'));
expect(widget.onFieldChanged).toHaveBeenCalledWith(field); expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
}); });
@@ -187,16 +183,15 @@ describe('DateWidgetComponent', () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
}); });
it('should show visible date widget', async () => { it('should show visible date widget', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9-9-9999', value: new Date('9-9-9999'),
type: 'date' type: FormFieldTypes.DATE
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).not.toBeNull(); expect(dateElement).not.toBeNull();
@@ -204,30 +199,20 @@ describe('DateWidgetComponent', () => {
expect(dateElement?.value).toContain('9-9-9999'); expect(dateElement?.value).toContain('9-9-9999');
}); });
it('[C310335] - Should be able to change display format for Date widget', async () => { it('[C310335] - Should be able to change display format for Date widget', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '30-12-9999', value: new Date('12-30-9999'),
type: 'date', type: FormFieldTypes.DATE,
dateDisplayFormat: 'MM-DD-YYYY' dateDisplayFormat: 'dd.MM.yyyy'
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
let dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); let dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement?.value).toContain('12-30-9999');
widget.field.value = '05-06-2019';
widget.field.dateDisplayFormat = 'DD.MM.YYYY';
fixture.componentInstance.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement?.value).toContain('05.06.2019'); expect(dateElement?.value).toContain('30.12.9999');
}); });
it('should disable date button when is readonly', () => { it('should disable date button when is readonly', () => {
@@ -235,7 +220,7 @@ describe('DateWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9-9-9999', value: '9-9-9999',
type: 'date' type: FormFieldTypes.DATE
}); });
fixture.detectChanges(); fixture.detectChanges();
@@ -257,41 +242,44 @@ describe('DateWidgetComponent', () => {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: 'aa', value: 'aa',
type: 'date', type: FormFieldTypes.DATE,
readOnly: 'false' readOnly: 'false'
}); });
widget.field.isVisible = true; widget.field.isVisible = true;
widget.field.readOnly = false; widget.field.readOnly = false;
fixture.detectChanges(); fixture.detectChanges();
widget.dateInputControl.setValue(new Date('invalid date'));
fixture.detectChanges();
expect(widget.field.isValid).toBeFalsy(); expect(widget.field.isValid).toBeFalsy();
}); });
}); });
it('should display always the json value', async () => { it('should display always the json value', () => {
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '30-12-9999', value: new Date('12-30-9999'),
type: 'date', type: FormFieldTypes.DATE,
dateDisplayFormat: 'MM-DD-YYYY' dateDisplayFormat: 'MM-dd-yyyy'
}); });
widget.field = field; widget.field = field;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).toBeDefined(); expect(dateElement).toBeDefined();
expect(dateElement.value).toContain('12-30-9999'); expect(dateElement.value).toContain('12-30-9999');
widget.field.value = '03-02-2020'; dateElement.value = '03-02-2020';
dateElement.dispatchEvent(new Event('input'));
fixture.componentInstance.ngOnInit(); fixture.componentInstance.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(dateElement.value).toContain('02-03-2020'); expect(dateElement.value).toContain('03-02-2020');
}); });
}); });

View File

@@ -18,18 +18,21 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { NgIf } from '@angular/common'; import { NgIf } from '@angular/common';
import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerInputEvent, MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { ADF_DATE_FORMATS, AdfDateFnsAdapter } from '../../../../common'; import { takeUntil } from 'rxjs/operators';
import { ADF_DATE_FORMATS, AdfDateFnsAdapter, DateFnsUtils, DEFAULT_DATE_FORMAT } from '../../../../common';
import { FormService } from '../../../services/form.service'; import { FormService } from '../../../services/form.service';
import { ErrorWidgetComponent } from '../error/error.component'; import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component'; import { WidgetComponent } from '../widget.component';
import { ErrorMessageModel } from '../core/error-message.model';
import { parseISO } from 'date-fns';
@Component({ @Component({
selector: 'date-widget', selector: 'date-widget',
@@ -50,62 +53,120 @@ import { WidgetComponent } from '../widget.component';
'(invalid)': 'event($event)', '(invalid)': 'event($event)',
'(select)': 'event($event)' '(select)': 'event($event)'
}, },
imports: [MatFormFieldModule, TranslateModule, MatInputModule, MatDatepickerModule, FormsModule, ErrorWidgetComponent, NgIf], imports: [MatFormFieldModule, TranslateModule, MatInputModule, MatDatepickerModule, ReactiveFormsModule, ErrorWidgetComponent, NgIf],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class DateWidgetComponent extends WidgetComponent implements OnInit, OnDestroy { export class DateWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
DATE_FORMAT = 'dd-MM-yyyy';
minDate: Date; minDate: Date;
maxDate: Date; maxDate: Date;
startAt: Date; startAt: Date;
@Input() dateInputControl: FormControl<Date> = new FormControl<Date>(null);
value: any = null;
private onDestroy$ = new Subject<boolean>(); private onDestroy$ = new Subject<void>();
constructor(public formService: FormService, private dateAdapter: DateAdapter<Date>) { public readonly formService = inject(FormService);
super(formService); private readonly dateAdapter = inject(DateAdapter);
ngOnInit(): void {
this.patchFormControl();
this.initDateAdapter();
this.initDateRange();
this.initStartAt();
this.subscribeToDateChanges();
this.updateField();
} }
ngOnInit() { updateField(): void {
if (this.field.dateDisplayFormat) { this.validateField();
this.onFieldChanged(this.field);
}
private patchFormControl(): void {
this.dateInputControl.setValue(this.field.value, { emitEvent: false });
this.dateInputControl.setValidators(this.isRequired() ? [Validators.required] : []);
if (this.field?.readOnly || this.readOnly) {
this.dateInputControl.disable({ emitEvent: false });
}
this.dateInputControl.updateValueAndValidity({ emitEvent: false });
}
private subscribeToDateChanges(): void {
this.dateInputControl.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe((newDate: Date) => {
this.field.value = newDate;
this.updateField();
});
}
private validateField(): void {
if (this.dateInputControl.invalid) {
this.handleErrors(this.dateInputControl.errors);
this.field.markAsInvalid();
} else {
this.resetErrors();
this.field.markAsValid();
}
}
private handleErrors(errors: ValidationErrors): void {
const errorAttributes = new Map<string, string>();
switch (true) {
case !!errors.matDatepickerParse:
this.updateValidationSummary(this.field.dateDisplayFormat || this.field.defaultDateTimeFormat);
break;
case !!errors.required:
this.updateValidationSummary('FORM.FIELD.REQUIRED');
break;
case !!errors.matDatepickerMin: {
const minValue = DateFnsUtils.formatDate(errors.matDatepickerMin.min, this.field.dateDisplayFormat).toLocaleUpperCase();
errorAttributes.set('minValue', minValue);
this.updateValidationSummary('FORM.FIELD.VALIDATOR.NOT_LESS_THAN', errorAttributes);
break;
}
case !!errors.matDatepickerMax: {
const maxValue = DateFnsUtils.formatDate(errors.matDatepickerMax.max, this.field.dateDisplayFormat).toLocaleUpperCase();
errorAttributes.set('maxValue', maxValue);
this.updateValidationSummary('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN', errorAttributes);
break;
}
default:
break;
}
}
private updateValidationSummary(message: string, attributes?: Map<string, string>): void {
this.field.validationSummary = new ErrorMessageModel({ message, attributes });
}
private resetErrors(): void {
this.updateValidationSummary('');
}
private initDateAdapter(): void {
if (this.field?.dateDisplayFormat) {
const adapter = this.dateAdapter as AdfDateFnsAdapter; const adapter = this.dateAdapter as AdfDateFnsAdapter;
adapter.displayFormat = this.field.dateDisplayFormat; adapter.displayFormat = this.field.dateDisplayFormat;
} }
}
if (this.field) { private initDateRange(): void {
if (this.field.minValue) { if (this.field?.minValue) {
this.minDate = this.dateAdapter.parse(this.field.minValue, this.DATE_FORMAT); this.minDate = parseISO(this.field.minValue);
} }
if (this.field.maxValue) { if (this.field?.maxValue) {
this.maxDate = this.dateAdapter.parse(this.field.maxValue, this.DATE_FORMAT); this.maxDate = parseISO(this.field.maxValue);
}
if (this.field.value) {
this.startAt = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
this.value = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
}
} }
} }
ngOnDestroy() { private initStartAt(): void {
this.onDestroy$.next(true); if (this.field?.value) {
this.startAt = this.dateAdapter.parse(this.field.value, DEFAULT_DATE_FORMAT);
}
}
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete(); this.onDestroy$.complete();
} }
onDateChange(event: MatDatepickerInputEvent<Date>) {
const value = event.value;
const input = event.targetElement as HTMLInputElement;
if (value) {
this.field.value = this.dateAdapter.format(value, this.DATE_FORMAT);
} else {
this.field.value = input.value;
}
this.onFieldChanged(this.field);
}
} }

View File

@@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnDestroy, HostListener, OnInit } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, OnDestroy, HostListener, OnInit, ChangeDetectorRef } from '@angular/core';
import { Observable, of, forkJoin, Subject, Subscription } from 'rxjs'; import { Observable, of, forkJoin, Subject, Subscription } from 'rxjs';
import { switchMap, takeUntil, map, filter } from 'rxjs/operators'; import { switchMap, takeUntil, map, filter } from 'rxjs/operators';
import { import {
@@ -130,7 +130,8 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
private dialog: MatDialog, private dialog: MatDialog,
protected visibilityService: WidgetVisibilityService, protected visibilityService: WidgetVisibilityService,
private readonly displayModeService: DisplayModeService, private readonly displayModeService: DisplayModeService,
private spinnerService: FormCloudSpinnerService private spinnerService: FormCloudSpinnerService,
private readonly changeDetector: ChangeDetectorRef
) { ) {
super(); super();
@@ -421,6 +422,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
this.displayModeOn.emit(this.displayModeService.findConfiguration(this.displayMode, this.displayModeConfigurations)); this.displayModeOn.emit(this.displayModeService.findConfiguration(this.displayMode, this.displayModeConfigurations));
} }
this.changeDetector.detectChanges();
this.formLoaded.emit(form); this.formLoaded.emit(form);
} }

View File

@@ -1,4 +1,4 @@
<div class="{{field.className}}" id="data-widget" [class.adf-invalid]="!field.isValid && isTouched()" [class.adf-left-label-input-container]="field.leftLabels"> <div class="{{field.className}}" id="data-widget" [class.adf-invalid]="dateInputControl.invalid && dateInputControl.touched" [class.adf-left-label-input-container]="field.leftLabels">
<div *ngIf="field.leftLabels"> <div *ngIf="field.leftLabels">
<label class="adf-label adf-left-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk" <label class="adf-label adf-left-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label> *ngIf="isRequired()">*</span></label>
@@ -7,24 +7,21 @@
<mat-form-field class="adf-date-widget" [class.adf-left-label-input-datepicker]="field.leftLabels" [hideRequiredMarker]="true"> <mat-form-field class="adf-date-widget" [class.adf-left-label-input-datepicker]="field.leftLabels" [hideRequiredMarker]="true">
<label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk" <label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label> *ngIf="isRequired()">*</span></label>
<input matInput [matDatepicker]="datePicker" <input matInput
[matDatepicker]="datePicker"
[id]="field.id" [id]="field.id"
[(ngModel)]="value" [formControl]="dateInputControl"
[required]="field.required"
[placeholder]="field.placeholder" [placeholder]="field.placeholder"
[min]="minDate" [min]="minDate"
[max]="maxDate" [max]="maxDate"
[disabled]="field.readOnly"
[title]="field.tooltip" [title]="field.tooltip"
(dateChange)="onDateChanged($event)" (blur)="updateField()">
(blur)="markAsTouched()">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly"></mat-datepicker-toggle>
<mat-datepicker #datePicker <mat-datepicker #datePicker
[startAt]="startAt" [startAt]="startAt"
[disabled]="field.readOnly"> [disabled]="field.readOnly">
</mat-datepicker> </mat-datepicker>
</mat-form-field> </mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget> <error-widget *ngIf="dateInputControl.invalid && dateInputControl.touched" [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div> </div>
</div> </div>

View File

@@ -17,7 +17,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DateCloudWidgetComponent } from './date-cloud.widget'; import { DateCloudWidgetComponent } from './date-cloud.widget';
import { FormFieldModel, FormModel, FormFieldTypes, DateFieldValidator, MinDateFieldValidator, MaxDateFieldValidator } from '@alfresco/adf-core'; import { FormFieldModel, FormModel, FormFieldTypes, DEFAULT_DATE_FORMAT } from '@alfresco/adf-core';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module'; import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { DateAdapter } from '@angular/material/core'; import { DateAdapter } from '@angular/material/core';
import { isEqual, subDays, addDays } from 'date-fns'; import { isEqual, subDays, addDays } from 'date-fns';
@@ -35,7 +35,6 @@ describe('DateWidgetComponent', () => {
}); });
form = new FormModel(); form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new MinDateFieldValidator(), new MaxDateFieldValidator()];
fixture = TestBed.createComponent(DateCloudWidgetComponent); fixture = TestBed.createComponent(DateCloudWidgetComponent);
adapter = fixture.debugElement.injector.get(DateAdapter); adapter = fixture.debugElement.injector.get(DateAdapter);
@@ -52,9 +51,9 @@ describe('DateWidgetComponent', () => {
minValue minValue
}); });
widget.ngOnInit(); fixture.detectChanges();
const expected = adapter.parse(minValue, widget.DATE_FORMAT); const expected = adapter.parse(minValue, DEFAULT_DATE_FORMAT);
expect(isEqual(widget.minDate, expected)).toBeTrue(); expect(isEqual(widget.minDate, expected)).toBeTrue();
}); });
@@ -77,9 +76,9 @@ describe('DateWidgetComponent', () => {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
maxValue maxValue
}); });
widget.ngOnInit(); fixture.detectChanges();
const expected = adapter.parse(maxValue, widget.DATE_FORMAT); const expected = adapter.parse(maxValue, DEFAULT_DATE_FORMAT);
expect(isEqual(widget.maxDate, expected)).toBeTrue(); expect(isEqual(widget.maxDate, expected)).toBeTrue();
}); });
@@ -95,7 +94,10 @@ describe('DateWidgetComponent', () => {
}); });
widget.field = field; widget.field = field;
widget.onDateChanged({ value: adapter.today() } as any);
fixture.detectChanges();
widget.dateInputControl.setValue(new Date('9999-9-9'));
expect(widget.onFieldChanged).toHaveBeenCalledWith(field); expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
}); });
@@ -106,17 +108,15 @@ describe('DateWidgetComponent', () => {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
}); });
it('should show visible date widget', async () => { it('should show visible date widget', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
// always stored as dd-MM-yyyy
value: '9999-9-9', value: '9999-9-9',
type: FormFieldTypes.DATE type: FormFieldTypes.DATE
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).not.toBeNull(); expect(dateElement).not.toBeNull();
@@ -128,10 +128,9 @@ describe('DateWidgetComponent', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
// always stored as dd-MM-yyyy value: new Date('12-30-9999'),
value: '30-12-9999',
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dateDisplayFormat: 'YYYY-DD-MM' dateDisplayFormat: 'yyyy-dd-MM'
}); });
fixture.detectChanges(); fixture.detectChanges();
@@ -141,31 +140,29 @@ describe('DateWidgetComponent', () => {
expect(dateElement.value).toContain('9999-30-12'); expect(dateElement.value).toContain('9999-30-12');
}); });
it('should disable date button when is readonly', async () => { it('should disable date button when is readonly', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
value: '9999-9-9', value: '9999-9-9',
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
readOnly: 'false' readOnly: false
}); });
widget.field.isVisible = true; widget.field.isVisible = true;
widget.field.readOnly = false; widget.field.readOnly = false;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
let dateButton = element.querySelector<HTMLButtonElement>('button'); let dateButton = element.querySelector<HTMLButtonElement>('button');
expect(dateButton.disabled).toBeFalsy(); expect(dateButton.disabled).toBeFalsy();
widget.field.readOnly = true; widget.field.readOnly = true;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
dateButton = element.querySelector<HTMLButtonElement>('button'); dateButton = element.querySelector<HTMLButtonElement>('button');
expect(dateButton.disabled).toBeTruthy(); expect(dateButton.disabled).toBeTruthy();
}); });
it('should set isValid to false when the value is not a correct date value', async () => { it('should set isValid to false when the value is not a correct date value', () => {
widget.field = new FormFieldModel(new FormModel(), { widget.field = new FormFieldModel(new FormModel(), {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
@@ -177,42 +174,43 @@ describe('DateWidgetComponent', () => {
widget.field.readOnly = false; widget.field.readOnly = false;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
widget.dateInputControl.setValue(new Date('invalid date'));
fixture.detectChanges();
expect(widget.field.isValid).toBeFalsy(); expect(widget.field.isValid).toBeFalsy();
}); });
}); });
it('should display always the json value', async () => { it('should display always the json value', () => {
const field = new FormFieldModel(form, { const field = new FormFieldModel(form, {
id: 'date-field-id', id: 'date-field-id',
name: 'date-name', name: 'date-name',
// always stored as dd-MM-yyyy value: new Date('12-30-9999'),
value: '30-12-9999',
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
readOnly: 'false', readOnly: false,
dateDisplayFormat: 'MM-DD-YYYY' dateDisplayFormat: 'MM-dd-yyyy'
}); });
widget.field = field; widget.field = field;
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id'); const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).toBeDefined(); expect(dateElement).toBeDefined();
expect(dateElement.value).toContain('12-30-9999'); expect(dateElement.value).toContain('12-30-9999');
widget.field.value = '03-02-2020'; dateElement.value = '03-02-2020';
dateElement.dispatchEvent(new Event('input'));
fixture.componentInstance.ngOnInit(); fixture.componentInstance.ngOnInit();
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(dateElement.value).toContain('02-03-2020'); expect(dateElement.value).toContain('03-02-2020');
}); });
describe('when form model has left labels', () => { describe('when form model has left labels', () => {
it('should have left labels classes on leftLabels true', async () => { it('should have left labels classes on leftLabels true', () => {
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), { widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
@@ -223,7 +221,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const widgetContainer = element.querySelector('.adf-left-label-input-container'); const widgetContainer = element.querySelector('.adf-left-label-input-container');
expect(widgetContainer).not.toBeNull(); expect(widgetContainer).not.toBeNull();
@@ -235,7 +232,7 @@ describe('DateWidgetComponent', () => {
expect(adfLeftLabel).not.toBeNull(); expect(adfLeftLabel).not.toBeNull();
}); });
it('should not have left labels classes on leftLabels false', async () => { it('should not have left labels classes on leftLabels false', () => {
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: false }), { widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: false }), {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
@@ -246,7 +243,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const widgetContainer = element.querySelector('.adf-left-label-input-container'); const widgetContainer = element.querySelector('.adf-left-label-input-container');
expect(widgetContainer).toBeNull(); expect(widgetContainer).toBeNull();
@@ -258,7 +254,7 @@ describe('DateWidgetComponent', () => {
expect(adfLeftLabel).toBeNull(); expect(adfLeftLabel).toBeNull();
}); });
it('should not have left labels classes on leftLabels not present', async () => { it('should not have left labels classes on leftLabels not present', () => {
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), { widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'date-id', id: 'date-id',
name: 'date-name', name: 'date-name',
@@ -269,7 +265,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const widgetContainer = element.querySelector('.adf-left-label-input-container'); const widgetContainer = element.querySelector('.adf-left-label-input-container');
expect(widgetContainer).toBeNull(); expect(widgetContainer).toBeNull();
@@ -291,7 +286,7 @@ describe('DateWidgetComponent', () => {
}); });
describe('Minimum date range value and date', () => { describe('Minimum date range value and date', () => {
it('should set minimum date range date to today if minimum date range value is 0', async () => { it('should set minimum date range date to today if minimum date range value is 0', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -300,7 +295,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expectedMinDate = adapter.today(); const expectedMinDate = adapter.today();
@@ -308,7 +302,7 @@ describe('DateWidgetComponent', () => {
expect(widget.field.minValue).toBe(todayString); expect(widget.field.minValue).toBe(todayString);
}); });
it('should set minimum date range date to null if minimum date range value is null', async () => { it('should set minimum date range date to null if minimum date range value is null', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -317,13 +311,12 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(widget.minDate).toBeNull(); expect(widget.minDate).toBeNull();
expect(widget.field.minValue).toBeNull(); expect(widget.field.minValue).toBeNull();
}); });
it('should set minimum date range date to today minus abs(minDateRangeValue) if minimum date range value is negative', async () => { it('should set minimum date range date to today minus abs(minDateRangeValue) if minimum date range value is negative', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -332,7 +325,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expectedMinDate = subDays(adapter.today(), 2); const expectedMinDate = subDays(adapter.today(), 2);
@@ -340,7 +332,7 @@ describe('DateWidgetComponent', () => {
expect(widget.field.minValue).toBe('20-02-2022'); expect(widget.field.minValue).toBe('20-02-2022');
}); });
it('should set minimum date range date to today plus minDateRangeValue if minimum date range value is positive', async () => { it('should set minimum date range date to today plus minDateRangeValue if minimum date range value is positive', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -349,7 +341,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expectedMinDate = addDays(adapter.today(), 2); const expectedMinDate = addDays(adapter.today(), 2);
@@ -359,7 +350,7 @@ describe('DateWidgetComponent', () => {
}); });
describe('Maximum date range value and date', () => { describe('Maximum date range value and date', () => {
it('should set maximum date range date to today if maximum date range value is 0', async () => { it('should set maximum date range date to today if maximum date range value is 0', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -368,7 +359,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expectedMaxDate = adapter.today(); const expectedMaxDate = adapter.today();
@@ -376,7 +366,7 @@ describe('DateWidgetComponent', () => {
expect(widget.field.maxValue).toBe(todayString); expect(widget.field.maxValue).toBe(todayString);
}); });
it('should set maximum date range date to null if maximum date range value is null', async () => { it('should set maximum date range date to null if maximum date range value is null', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -385,13 +375,12 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(widget.maxDate).toBeNull(); expect(widget.maxDate).toBeNull();
expect(widget.field.maxValue).toBeNull(); expect(widget.field.maxValue).toBeNull();
}); });
it('should set maximum date range date to today minus abs(maxDateRangeValue) if maximum date range value is negative', async () => { it('should set maximum date range date to today minus abs(maxDateRangeValue) if maximum date range value is negative', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -400,7 +389,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expectedMaxDate = subDays(adapter.today(), 2); const expectedMaxDate = subDays(adapter.today(), 2);
@@ -408,7 +396,7 @@ describe('DateWidgetComponent', () => {
expect(widget.field.maxValue).toBe('20-02-2022'); expect(widget.field.maxValue).toBe('20-02-2022');
}); });
it('should set maximum date range date to today plus maxDateRangeValue if maximum date range value is positive', async () => { it('should set maximum date range date to today plus maxDateRangeValue if maximum date range value is positive', () => {
widget.field = new FormFieldModel(form, { widget.field = new FormFieldModel(form, {
type: FormFieldTypes.DATE, type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true, dynamicDateRangeSelection: true,
@@ -417,7 +405,6 @@ describe('DateWidgetComponent', () => {
}); });
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const expectedMaxDate = addDays(adapter.today(), 2); const expectedMaxDate = addDays(adapter.today(), 2);
@@ -435,9 +422,8 @@ describe('DateWidgetComponent', () => {
}); });
}); });
it('should be able to display label with asterisk', async () => { it('should be able to display label with asterisk', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk'); const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
@@ -445,9 +431,8 @@ describe('DateWidgetComponent', () => {
expect(asterisk.textContent).toEqual('*'); expect(asterisk.textContent).toEqual('*');
}); });
it('should be invalid after user interaction without typing', async () => { it('should be invalid after user interaction without typing', () => {
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeFalsy(); expect(element.querySelector('.adf-invalid')).toBeFalsy();
@@ -455,7 +440,6 @@ describe('DateWidgetComponent', () => {
dateCloudInput.dispatchEvent(new Event('blur')); dateCloudInput.dispatchEvent(new Event('blur'));
fixture.detectChanges(); fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeTruthy(); expect(element.querySelector('.adf-invalid')).toBeTruthy();
}); });

View File

@@ -17,15 +17,42 @@
/* eslint-disable @angular-eslint/component-selector */ /* eslint-disable @angular-eslint/component-selector */
import { Component, OnInit, ViewEncapsulation, OnDestroy, Input } from '@angular/core'; import { Component, OnInit, ViewEncapsulation, OnDestroy, inject } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { WidgetComponent, FormService, AdfDateFnsAdapter, DateFnsUtils, ADF_DATE_FORMATS } from '@alfresco/adf-core'; import { takeUntil } from 'rxjs/operators';
import { MatDatepickerInputEvent } from '@angular/material/datepicker'; import {
import { addDays } from 'date-fns'; WidgetComponent,
FormService,
AdfDateFnsAdapter,
DateFnsUtils,
ADF_DATE_FORMATS,
ErrorWidgetComponent,
ErrorMessageModel,
DEFAULT_DATE_FORMAT
} from '@alfresco/adf-core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { addDays, parseISO } from 'date-fns';
import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
@Component({ @Component({
selector: 'date-widget', selector: 'date-widget',
standalone: true,
imports: [
NgIf,
TranslateModule,
MatFormFieldModule,
MatInputModule,
MatDatepickerModule,
MatTooltipModule,
ReactiveFormsModule,
ErrorWidgetComponent
],
providers: [ providers: [
{ provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS }, { provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: DateAdapter, useClass: AdfDateFnsAdapter } { provide: DateAdapter, useClass: AdfDateFnsAdapter }
@@ -47,80 +74,143 @@ import { addDays } from 'date-fns';
}) })
export class DateCloudWidgetComponent extends WidgetComponent implements OnInit, OnDestroy { export class DateCloudWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
typeId = 'DateCloudWidgetComponent'; typeId = 'DateCloudWidgetComponent';
readonly DATE_FORMAT = 'dd-MM-yyyy';
minDate: Date = null; minDate: Date = null;
maxDate: Date = null; maxDate: Date = null;
startAt: Date = null; startAt: Date = null;
/** dateInputControl: FormControl<Date> = new FormControl<Date>(null);
* Current date value.
* The value is always stored in the format `dd-MM-yyyy`,
* but displayed in the UI component using `dateDisplayFormat`
*/
@Input()
value: any = null;
private onDestroy$ = new Subject<boolean>(); private onDestroy$ = new Subject<void>();
constructor(public formService: FormService, private dateAdapter: DateAdapter<Date>) { public readonly formService = inject(FormService);
super(formService); private readonly dateAdapter = inject(DateAdapter);
ngOnInit(): void {
this.patchFormControl();
this.initDateAdapter();
this.initRangeSelection();
this.initStartAt();
this.subscribeToDateChanges();
this.updateField();
} }
ngOnInit() { updateField(): void {
if (this.field.dateDisplayFormat) { this.validateField();
this.onFieldChanged(this.field);
}
private patchFormControl(): void {
this.dateInputControl.setValue(this.field.value, { emitEvent: false });
this.dateInputControl.setValidators(this.isRequired() ? [Validators.required] : []);
if (this.field?.readOnly || this.readOnly) {
this.dateInputControl.disable({ emitEvent: false });
}
this.dateInputControl.updateValueAndValidity({ emitEvent: false });
}
private subscribeToDateChanges(): void {
this.dateInputControl.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe((newDate: Date) => {
this.field.value = newDate;
this.updateField();
});
}
private validateField(): void {
if (this.dateInputControl.invalid) {
this.handleErrors(this.dateInputControl.errors);
this.field.markAsInvalid();
} else {
this.resetErrors();
this.field.markAsValid();
}
}
private handleErrors(errors: ValidationErrors): void {
const errorAttributes = new Map<string, string>();
switch (true) {
case !!errors.matDatepickerParse:
this.updateValidationSummary(this.field.dateDisplayFormat || this.field.defaultDateTimeFormat);
break;
case !!errors.required:
this.updateValidationSummary('FORM.FIELD.REQUIRED');
break;
case !!errors.matDatepickerMin: {
const minValue = DateFnsUtils.formatDate(errors.matDatepickerMin.min, this.field.dateDisplayFormat).toLocaleUpperCase();
errorAttributes.set('minValue', minValue);
this.updateValidationSummary('FORM.FIELD.VALIDATOR.NOT_LESS_THAN', errorAttributes);
break;
}
case !!errors.matDatepickerMax: {
const maxValue = DateFnsUtils.formatDate(errors.matDatepickerMax.max, this.field.dateDisplayFormat).toLocaleUpperCase();
errorAttributes.set('maxValue', maxValue);
this.updateValidationSummary('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN', errorAttributes);
break;
}
default:
break;
}
}
private updateValidationSummary(message: string, attributes?: Map<string, string>): void {
this.field.validationSummary = new ErrorMessageModel({ message, attributes });
}
private resetErrors(): void {
this.updateValidationSummary('');
}
private initDateAdapter(): void {
if (this.field?.dateDisplayFormat) {
const adapter = this.dateAdapter as AdfDateFnsAdapter; const adapter = this.dateAdapter as AdfDateFnsAdapter;
adapter.displayFormat = this.field.dateDisplayFormat; adapter.displayFormat = this.field.dateDisplayFormat;
} }
}
if (this.field) { private initStartAt(): void {
if (this.field.dynamicDateRangeSelection) { if (this.field?.value) {
if (this.field.minDateRangeValue === null) { this.startAt = this.dateAdapter.parse(this.field.value, DEFAULT_DATE_FORMAT);
this.minDate = null;
this.field.minValue = null;
} else {
this.minDate = addDays(this.dateAdapter.today(), this.field.minDateRangeValue);
this.field.minValue = DateFnsUtils.formatDate(this.minDate, this.DATE_FORMAT);
}
if (this.field.maxDateRangeValue === null) {
this.maxDate = null;
this.field.maxValue = null;
} else {
this.maxDate = addDays(this.dateAdapter.today(), this.field.maxDateRangeValue);
this.field.maxValue = DateFnsUtils.formatDate(this.maxDate, this.DATE_FORMAT);
}
} else {
if (this.field.minValue) {
this.minDate = this.dateAdapter.parse(this.field.minValue, this.DATE_FORMAT);
}
if (this.field.maxValue) {
this.maxDate = this.dateAdapter.parse(this.field.maxValue, this.DATE_FORMAT);
}
}
if (this.field.value) {
this.startAt = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
this.value = this.dateAdapter.parse(this.field.value, this.DATE_FORMAT);
}
} }
} }
ngOnDestroy() { private initRangeSelection(): void {
this.onDestroy$.next(true); if (this.field?.dynamicDateRangeSelection) {
this.setDynamicRangeSelection();
} else {
this.setStaticRangeSelection();
}
}
private setDynamicRangeSelection(): void {
if (this.field.minDateRangeValue === null) {
this.minDate = null;
this.field.minValue = null;
} else {
this.minDate = addDays(this.dateAdapter.today(), this.field.minDateRangeValue);
this.field.minValue = DateFnsUtils.formatDate(this.minDate, DEFAULT_DATE_FORMAT);
}
if (this.field.maxDateRangeValue === null) {
this.maxDate = null;
this.field.maxValue = null;
} else {
this.maxDate = addDays(this.dateAdapter.today(), this.field.maxDateRangeValue);
this.field.maxValue = DateFnsUtils.formatDate(this.maxDate, DEFAULT_DATE_FORMAT);
}
}
private setStaticRangeSelection(): void {
if (this.field?.minValue) {
this.minDate = parseISO(this.field.minValue);
}
if (this.field?.maxValue) {
this.maxDate = parseISO(this.field.maxValue);
}
}
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete(); this.onDestroy$.complete();
} }
onDateChanged(event: MatDatepickerInputEvent<Date>) {
const value = event.value;
const input = event.targetElement as HTMLInputElement;
if (value) {
this.field.value = this.dateAdapter.format(value, this.DATE_FORMAT);
} else {
this.field.value = input.value;
}
this.onFieldChanged(this.field);
}
} }

View File

@@ -29,8 +29,6 @@ import {
CONTENT_UPLOAD_DIRECTIVES, CONTENT_UPLOAD_DIRECTIVES,
ContentNodeSelectorModule ContentNodeSelectorModule
} from '@alfresco/adf-content-services'; } from '@alfresco/adf-content-services';
import { DateCloudWidgetComponent } from './components/widgets/date/date-cloud.widget';
import { DropdownCloudWidgetComponent } from './components/widgets/dropdown/dropdown-cloud.widget'; import { DropdownCloudWidgetComponent } from './components/widgets/dropdown/dropdown-cloud.widget';
import { GroupCloudWidgetComponent } from './components/widgets/group/group-cloud.widget'; import { GroupCloudWidgetComponent } from './components/widgets/group/group-cloud.widget';
import { PeopleCloudWidgetComponent } from './components/widgets/people/people-cloud.widget'; import { PeopleCloudWidgetComponent } from './components/widgets/people/people-cloud.widget';
@@ -78,7 +76,6 @@ import { FormCloudSpinnerService } from './services/spinner/form-cloud-spinner.s
DropdownCloudWidgetComponent, DropdownCloudWidgetComponent,
RadioButtonsCloudWidgetComponent, RadioButtonsCloudWidgetComponent,
AttachFileCloudWidgetComponent, AttachFileCloudWidgetComponent,
DateCloudWidgetComponent,
PeopleCloudWidgetComponent, PeopleCloudWidgetComponent,
GroupCloudWidgetComponent, GroupCloudWidgetComponent,
PropertiesViewerWrapperComponent, PropertiesViewerWrapperComponent,
@@ -96,7 +93,6 @@ import { FormCloudSpinnerService } from './services/spinner/form-cloud-spinner.s
DropdownCloudWidgetComponent, DropdownCloudWidgetComponent,
RadioButtonsCloudWidgetComponent, RadioButtonsCloudWidgetComponent,
AttachFileCloudWidgetComponent, AttachFileCloudWidgetComponent,
DateCloudWidgetComponent,
PeopleCloudWidgetComponent, PeopleCloudWidgetComponent,
GroupCloudWidgetComponent, GroupCloudWidgetComponent,
PropertiesViewerWidgetComponent, PropertiesViewerWidgetComponent,