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 { MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatDateFormats } from '@angular/material/core';
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.
@@ -47,15 +47,17 @@ import { Locale } from 'date-fns';
* }
*/
export const DEFAULT_DATE_FORMAT = 'dd-MM-yyyy';
/**
* Material date formats for Date-fns
*/
export const ADF_DATE_FORMATS: MatDateFormats = {
parse: {
dateInput: 'dd-MM-yyyy'
dateInput: DEFAULT_DATE_FORMAT
},
display: {
dateInput: 'dd-MM-yyyy',
dateInput: DEFAULT_DATE_FORMAT,
monthLabel: 'LLL',
monthYearLabel: 'LLL uuuu',
dateA11yLabel: 'PP',
@@ -88,10 +90,11 @@ export class AdfDateFnsAdapter extends DateFnsAdapter {
}
override parse(value: any, parseFormat: string | string[]): Date {
const dateValue = this.isValid(value) ? value : this.parseAndValidateDate(value);
const format = Array.isArray(parseFormat)
? parseFormat.map(DateFnsUtils.convertMomentToDateFnsFormat)
: DateFnsUtils.convertMomentToDateFnsFormat(parseFormat);
return super.parse(value, format);
return super.parse(dateValue, format);
}
override format(date: Date, displayFormat: string): string {
@@ -103,4 +106,9 @@ export class AdfDateFnsAdapter extends DateFnsAdapter {
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 { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimeFormats } from '@mat-datetimepicker/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)
@@ -126,7 +126,8 @@ export class AdfDateTimeFnsAdapter extends DatetimeAdapter<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 {

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,6 @@
*/
import { ErrorMessageModel } from './error-message.model';
import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types';
import {
FixedValueFieldValidator,
@@ -27,11 +26,6 @@ import {
NumberFieldValidator,
RegExFieldValidator,
RequiredFieldValidator,
MaxDateTimeFieldValidator,
MinDateTimeFieldValidator,
MaxDateFieldValidator,
MinDateFieldValidator,
DateTimeFieldValidator,
DecimalFieldValidator
} from './form-field-validator';
import { FormFieldModel } from './form-field.model';
@@ -65,22 +59,6 @@ describe('FormFieldValidator', () => {
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', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
@@ -196,20 +174,6 @@ describe('FormFieldValidator', () => {
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', () => {
const field = new FormFieldModel(new FormModel(), {
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', () => {
let decimalValidator: DecimalFieldValidator;

View File

@@ -20,8 +20,6 @@
import { FormFieldTypes } from './form-field-types';
import { isNumberValue } from './form-field-utils';
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 {
isSupported(field: FormFieldModel): boolean;
@@ -42,8 +40,6 @@ export class RequiredFieldValidator implements FormFieldValidator {
FormFieldTypes.UPLOAD,
FormFieldTypes.AMOUNT,
FormFieldTypes.DYNAMIC_TABLE,
FormFieldTypes.DATE,
FormFieldTypes.DATETIME,
FormFieldTypes.ATTACH_FOLDER,
FormFieldTypes.DECIMAL,
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 {
private supportedTypes = [FormFieldTypes.TEXT, FormFieldTypes.MULTILINE_TEXT];
@@ -523,12 +317,6 @@ export const FORM_FIELD_VALIDATORS = [
new MinValueFieldValidator(),
new MaxValueFieldValidator(),
new RegExFieldValidator(),
new DateFieldValidator(),
new DateTimeFieldValidator(),
new MinDateFieldValidator(),
new MaxDateFieldValidator(),
new FixedValueFieldValidator(),
new MinDateTimeFieldValidator(),
new MaxDateTimeFieldValidator(),
new DecimalFieldValidator()
];

View File

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

View File

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

View File

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

View File

@@ -22,12 +22,10 @@ import { DateTimeWidgetComponent } from './date-time.widget';
import { TranslateModule } from '@ngx-translate/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FormFieldTypes } from '../core/form-field-types';
import { DateFieldValidator, DateTimeFieldValidator } from '../core';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatInputHarness } from '@angular/material/input/testing';
import { addMinutes } from 'date-fns';
import { HttpClientModule } from '@angular/common/http';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core';
@@ -35,6 +33,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('DateTimeWidgetComponent', () => {
let loader: HarnessLoader;
@@ -47,7 +46,7 @@ describe('DateTimeWidgetComponent', () => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
HttpClientModule,
HttpClientTestingModule,
NoopAnimationsModule,
MatDialogModule,
MatMenuModule,
@@ -65,7 +64,6 @@ describe('DateTimeWidgetComponent', () => {
widget = fixture.componentInstance;
form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new DateTimeFieldValidator()];
loader = TestbedHarnessEnvironment.loader(fixture);
});
@@ -74,17 +72,16 @@ describe('DateTimeWidgetComponent', () => {
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';
widget.field = new FormFieldModel(form, {
id: 'date-id',
name: 'date-name',
type: 'datetime',
type: FormFieldTypes.DATETIME,
minValue
});
fixture.detectChanges();
await fixture.whenStable();
expect(widget.minDate.toISOString()).toBe(`1982-03-13T10:00:00.000Z`);
});
@@ -93,7 +90,7 @@ describe('DateTimeWidgetComponent', () => {
widget.field = new FormFieldModel(form, {
id: 'date-id',
name: 'date-name',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
fixture.detectChanges();
@@ -101,13 +98,12 @@ describe('DateTimeWidgetComponent', () => {
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';
widget.field = new FormFieldModel(null, {
maxValue
});
fixture.detectChanges();
await fixture.whenStable();
expect(widget.maxDate.toISOString()).toBe('1982-03-13T10:00:00.000Z');
});
@@ -119,76 +115,70 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
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);
});
it('should validate the initial datetime value', async () => {
it('should validate the initial datetime value', () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
widget.field = field;
fixture.whenStable();
await fixture.whenStable();
fixture.detectChanges();
expect(field.isValid).toBeTrue();
});
it('should validate the updated datetime value', async () => {
it('should validate the updated datetime value', () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
widget.field = field;
fixture.whenStable();
await fixture.whenStable();
fixture.detectChanges();
let expectedDate = new Date('9999-09-12T09:10:00.000Z');
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();
});
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, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
widget.field = field;
fixture.detectChanges();
await fixture.whenStable();
widget.onDateChanged({
value: null,
targetElement: {
value: '123abc'
}
} as any);
widget.datetimeInputControl.setValue(new Date('123abc'));
fixture.detectChanges();
await fixture.whenStable();
expect(field.value).toBe('123abc');
expect(widget.datetimeInputControl.invalid).toBeTrue();
expect(field.isValid).toBeFalse();
expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A');
});
@@ -198,7 +188,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
widget.field = field;
@@ -206,35 +196,31 @@ describe('DateTimeWidgetComponent', () => {
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();
});
it('should fail validating incorrect keyboard input', async () => {
it('should fail validating incorrect keyboard input', () => {
const field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
widget.field = field;
fixture.detectChanges();
await fixture.whenStable();
widget.onValueChanged({
target: {
value: '123abc'
}
} as any);
const dateTimeInput = fixture.nativeElement.querySelector('input');
dateTimeInput.value = '123abc';
dateTimeInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(field.value).toBe('123abc');
expect(widget.datetimeInputControl.invalid).toBeTrue();
expect(field.value).toBe(null);
expect(field.isValid).toBeFalse();
expect(field.validationSummary.message).toBe('D-M-YYYY hh:mm A');
});
@@ -244,7 +230,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id',
name: 'date-name',
value: '9999-09-12T09:00:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
widget.field = field;
@@ -252,9 +238,12 @@ describe('DateTimeWidgetComponent', () => {
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();
});
@@ -282,6 +271,7 @@ describe('DateTimeWidgetComponent', () => {
type: FormFieldTypes.DATETIME,
required: true
});
fixture.detectChanges();
});
it('should be marked as invalid after interaction', () => {
@@ -296,8 +286,6 @@ describe('DateTimeWidgetComponent', () => {
});
it('should be able to display label with asterisk', () => {
fixture.detectChanges();
const asterisk = element.querySelector<HTMLElement>('.adf-asterisk');
expect(asterisk).not.toBeNull();
@@ -311,7 +299,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id',
name: 'date-name',
value: '9999-11-30T10:30:00.000Z',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
fixture.detectChanges();
@@ -327,7 +315,7 @@ describe('DateTimeWidgetComponent', () => {
name: 'date-name',
value: '9999-12-30T10:30:00.000Z',
dateDisplayFormat: 'MM-DD-YYYY HH:mm A',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
fixture.detectChanges();
@@ -343,7 +331,7 @@ describe('DateTimeWidgetComponent', () => {
name: 'date-name',
value: '9999-12-30T10:30:00.000Z',
dateDisplayFormat: 'MM-DD-YYYY HH:mm A',
type: 'datetime'
type: FormFieldTypes.DATETIME
});
fixture.detectChanges();
@@ -365,7 +353,7 @@ describe('DateTimeWidgetComponent', () => {
id: 'date-field-id',
name: 'datetime-field-name',
value: '9999-12-30T10:30:00.000Z',
type: 'datetime',
type: FormFieldTypes.DATETIME,
dateDisplayFormat: 'MM-DD-YYYY HH:mm A'
});
widget.field = field;
@@ -385,6 +373,31 @@ describe('DateTimeWidgetComponent', () => {
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', () => {
it('should have left labels classes on 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 */
import { NgIf } from '@angular/common';
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
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 { isValid } from 'date-fns';
import { ADF_DATE_FORMATS, ADF_DATETIME_FORMATS, AdfDateFnsAdapter, AdfDateTimeFnsAdapter, DateFnsUtils } from '../../../../common';
import { FormService } from '../../../services/form.service';
import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ErrorMessageModel } from '../core/error-message.model';
@Component({
selector: 'date-time-widget',
@@ -42,68 +44,116 @@ import { WidgetComponent } from '../widget.component';
],
templateUrl: './date-time.widget.html',
styleUrls: ['./date-time.widget.scss'],
imports: [NgIf, TranslateModule, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, FormsModule, ErrorWidgetComponent],
imports: [NgIf, TranslateModule, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, ReactiveFormsModule, ErrorWidgetComponent],
encapsulation: ViewEncapsulation.None
})
export class DateTimeWidgetComponent extends WidgetComponent implements OnInit {
export class DateTimeWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
minDate: Date;
maxDate: Date;
datetimeInputControl: FormControl<Date> = new FormControl<Date>(null);
@Input()
value: any = null;
private onDestroy$ = new Subject<void>();
constructor(public formService: FormService, private dateAdapter: DateAdapter<Date>, private dateTimeAdapter: DatetimeAdapter<Date>) {
super(formService);
public readonly formService = inject(FormService);
private readonly dateAdapter = inject(DateAdapter);
private readonly dateTimeAdapter = inject(DatetimeAdapter);
ngOnInit(): void {
this.patchFormControl();
this.initDateAdapter();
this.initDateRange();
this.subscribeToDateChanges();
this.updateField();
}
ngOnInit() {
if (this.field.dateDisplayFormat) {
updateField(): void {
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;
dateAdapter.displayFormat = this.field.dateDisplayFormat;
const dateTimeAdapter = this.dateTimeAdapter as AdfDateTimeFnsAdapter;
dateTimeAdapter.displayFormat = this.field.dateDisplayFormat;
}
}
if (this.field) {
if (this.field.minValue) {
private initDateRange(): void {
if (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);
}
if (this.field.value) {
this.value = DateFnsUtils.getDate(this.field.value);
}
}
}
onValueChanged(event: Event) {
const input = event.target as HTMLInputElement;
const newValue = this.dateTimeAdapter.parse(input.value, this.field.dateDisplayFormat);
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);
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete();
}
}

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

View File

@@ -18,8 +18,10 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DateAdapter } from '@angular/material/core';
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 { DEFAULT_DATE_FORMAT } from '../../../../common';
import { isEqual } from 'date-fns';
describe('DateWidgetComponent', () => {
let widget: DateWidgetComponent;
@@ -34,7 +36,6 @@ describe('DateWidgetComponent', () => {
});
form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new MinDateFieldValidator(), new MaxDateFieldValidator()];
fixture = TestBed.createComponent(DateWidgetComponent);
adapter = fixture.debugElement.injector.get(DateAdapter);
@@ -54,8 +55,8 @@ describe('DateWidgetComponent', () => {
});
it('should setup min value for date picker', () => {
const minValue = '13-03-1982';
widget.field = new FormFieldModel(form, {
const minValue = '1982-03-13';
widget.field = new FormFieldModel(null, {
id: 'date-id',
name: 'date-name',
minValue
@@ -63,12 +64,12 @@ describe('DateWidgetComponent', () => {
widget.ngOnInit();
const expected = adapter.parse(minValue, widget.DATE_FORMAT) as Date;
expect(adapter.compareDate(widget.minDate, expected)).toBe(0);
const expected = adapter.parse(minValue, DEFAULT_DATE_FORMAT);
expect(isEqual(widget.minDate, expected)).toBeTrue();
});
it('should validate min date value constraint', async () => {
const minValue = '13-03-1982';
it('should validate min date value constraint', () => {
const minValue = '1982-03-13';
const field = new FormFieldModel(form, {
id: 'date-id',
@@ -79,22 +80,19 @@ describe('DateWidgetComponent', () => {
});
widget.field = field;
widget.ngOnInit();
fixture.detectChanges();
widget.onDateChange({
value: new Date('1982/03/12')
} as any);
widget.dateInputControl.setValue(new Date('1982/03/12'));
fixture.detectChanges();
await fixture.whenStable();
expect(widget.field.isValid).toBeFalsy();
expect(field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
expect(field.validationSummary.attributes.get('minValue')).toBe('13-03-1982');
});
it('should validate max date value constraint', async () => {
const maxValue = '13-03-1982';
it('should validate max date value constraint', () => {
const maxValue = '1982-03-13';
const field = new FormFieldModel(form, {
id: 'date-id',
@@ -105,14 +103,11 @@ describe('DateWidgetComponent', () => {
});
widget.field = field;
widget.ngOnInit();
fixture.detectChanges();
widget.onDateChange({
value: new Date('2023/03/13')
} as any);
widget.dateInputControl.setValue(new Date('2023/03/13'));
fixture.detectChanges();
await fixture.whenStable();
expect(widget.field.isValid).toBeFalsy();
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', () => {
const maxValue = '31-03-1982';
const maxValue = '1982-03-31';
widget.field = new FormFieldModel(form, {
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);
});
@@ -153,9 +148,10 @@ describe('DateWidgetComponent', () => {
});
widget.field = field;
widget.onDateChange({
value: new Date('12/12/2012')
} as any);
fixture.detectChanges();
widget.dateInputControl.setValue(new Date('12/12/2012'));
expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
});
@@ -187,16 +183,15 @@ describe('DateWidgetComponent', () => {
TestBed.resetTestingModule();
});
it('should show visible date widget', async () => {
it('should show visible date widget', () => {
widget.field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
value: '9-9-9999',
type: 'date'
value: new Date('9-9-9999'),
type: FormFieldTypes.DATE
});
fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).not.toBeNull();
@@ -204,30 +199,20 @@ describe('DateWidgetComponent', () => {
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, {
id: 'date-field-id',
name: 'date-name',
value: '30-12-9999',
type: 'date',
dateDisplayFormat: 'MM-DD-YYYY'
value: new Date('12-30-9999'),
type: FormFieldTypes.DATE,
dateDisplayFormat: 'dd.MM.yyyy'
});
fixture.detectChanges();
await fixture.whenStable();
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');
expect(dateElement?.value).toContain('05.06.2019');
expect(dateElement?.value).toContain('30.12.9999');
});
it('should disable date button when is readonly', () => {
@@ -235,7 +220,7 @@ describe('DateWidgetComponent', () => {
id: 'date-field-id',
name: 'date-name',
value: '9-9-9999',
type: 'date'
type: FormFieldTypes.DATE
});
fixture.detectChanges();
@@ -257,41 +242,44 @@ describe('DateWidgetComponent', () => {
id: 'date-field-id',
name: 'date-name',
value: 'aa',
type: 'date',
type: FormFieldTypes.DATE,
readOnly: 'false'
});
widget.field.isVisible = true;
widget.field.readOnly = false;
fixture.detectChanges();
widget.dateInputControl.setValue(new Date('invalid date'));
fixture.detectChanges();
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, {
id: 'date-field-id',
name: 'date-name',
value: '30-12-9999',
type: 'date',
dateDisplayFormat: 'MM-DD-YYYY'
value: new Date('12-30-9999'),
type: FormFieldTypes.DATE,
dateDisplayFormat: 'MM-dd-yyyy'
});
widget.field = field;
fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).toBeDefined();
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.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 */
import { NgIf } from '@angular/common';
import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
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 { MatInputModule } from '@angular/material/input';
import { TranslateModule } from '@ngx-translate/core';
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 { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { ErrorMessageModel } from '../core/error-message.model';
import { parseISO } from 'date-fns';
@Component({
selector: 'date-widget',
@@ -50,62 +53,120 @@ import { WidgetComponent } from '../widget.component';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [MatFormFieldModule, TranslateModule, MatInputModule, MatDatepickerModule, FormsModule, ErrorWidgetComponent, NgIf],
imports: [MatFormFieldModule, TranslateModule, MatInputModule, MatDatepickerModule, ReactiveFormsModule, ErrorWidgetComponent, NgIf],
encapsulation: ViewEncapsulation.None
})
export class DateWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
DATE_FORMAT = 'dd-MM-yyyy';
minDate: Date;
maxDate: Date;
startAt: Date;
@Input()
value: any = null;
dateInputControl: FormControl<Date> = new FormControl<Date>(null);
private onDestroy$ = new Subject<boolean>();
private onDestroy$ = new Subject<void>();
constructor(public formService: FormService, private dateAdapter: DateAdapter<Date>) {
super(formService);
public readonly formService = inject(FormService);
private readonly dateAdapter = inject(DateAdapter);
ngOnInit(): void {
this.patchFormControl();
this.initDateAdapter();
this.initDateRange();
this.initStartAt();
this.subscribeToDateChanges();
this.updateField();
}
ngOnInit() {
if (this.field.dateDisplayFormat) {
updateField(): void {
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;
adapter.displayFormat = this.field.dateDisplayFormat;
}
if (this.field) {
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);
private initDateRange(): void {
if (this.field?.minValue) {
this.minDate = parseISO(this.field.minValue);
}
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);
}
if (this.field?.maxValue) {
this.maxDate = parseISO(this.field.maxValue);
}
}
ngOnDestroy() {
this.onDestroy$.next(true);
private initStartAt(): void {
if (this.field?.value) {
this.startAt = this.dateAdapter.parse(this.field.value, DEFAULT_DATE_FORMAT);
}
}
ngOnDestroy(): void {
this.onDestroy$.next();
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.
*/
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 { switchMap, takeUntil, map, filter } from 'rxjs/operators';
import {
@@ -130,7 +130,8 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
private dialog: MatDialog,
protected visibilityService: WidgetVisibilityService,
private readonly displayModeService: DisplayModeService,
private spinnerService: FormCloudSpinnerService
private spinnerService: FormCloudSpinnerService,
private readonly changeDetector: ChangeDetectorRef
) {
super();
@@ -421,6 +422,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
this.displayModeOn.emit(this.displayModeService.findConfiguration(this.displayMode, this.displayModeConfigurations));
}
this.changeDetector.detectChanges();
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">
<label class="adf-label adf-left-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*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">
<label class="adf-label" *ngIf="!field.leftLabels" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<input matInput [matDatepicker]="datePicker"
<input matInput
[matDatepicker]="datePicker"
[id]="field.id"
[(ngModel)]="value"
[required]="field.required"
[formControl]="dateInputControl"
[placeholder]="field.placeholder"
[min]="minDate"
[max]="maxDate"
[disabled]="field.readOnly"
[title]="field.tooltip"
(dateChange)="onDateChanged($event)"
(blur)="markAsTouched()">
(blur)="updateField()">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly"></mat-datepicker-toggle>
<mat-datepicker #datePicker
[startAt]="startAt"
[disabled]="field.readOnly">
</mat-datepicker>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="dateInputControl.invalid && dateInputControl.touched" [error]="field.validationSummary"></error-widget>
</div>
</div>

View File

@@ -17,7 +17,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
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 { DateAdapter } from '@angular/material/core';
import { isEqual, subDays, addDays } from 'date-fns';
@@ -35,7 +35,6 @@ describe('DateWidgetComponent', () => {
});
form = new FormModel();
form.fieldValidators = [new DateFieldValidator(), new MinDateFieldValidator(), new MaxDateFieldValidator()];
fixture = TestBed.createComponent(DateCloudWidgetComponent);
adapter = fixture.debugElement.injector.get(DateAdapter);
@@ -52,9 +51,9 @@ describe('DateWidgetComponent', () => {
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();
});
@@ -77,9 +76,9 @@ describe('DateWidgetComponent', () => {
type: FormFieldTypes.DATE,
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();
});
@@ -95,7 +94,10 @@ describe('DateWidgetComponent', () => {
});
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);
});
@@ -106,17 +108,15 @@ describe('DateWidgetComponent', () => {
TestBed.resetTestingModule();
});
it('should show visible date widget', async () => {
it('should show visible date widget', () => {
widget.field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
// always stored as dd-MM-yyyy
value: '9999-9-9',
type: FormFieldTypes.DATE
});
fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).not.toBeNull();
@@ -128,10 +128,9 @@ describe('DateWidgetComponent', () => {
widget.field = new FormFieldModel(form, {
id: 'date-field-id',
name: 'date-name',
// always stored as dd-MM-yyyy
value: '30-12-9999',
value: new Date('12-30-9999'),
type: FormFieldTypes.DATE,
dateDisplayFormat: 'YYYY-DD-MM'
dateDisplayFormat: 'yyyy-dd-MM'
});
fixture.detectChanges();
@@ -141,31 +140,29 @@ describe('DateWidgetComponent', () => {
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, {
id: 'date-field-id',
name: 'date-name',
value: '9999-9-9',
type: FormFieldTypes.DATE,
readOnly: 'false'
readOnly: false
});
widget.field.isVisible = true;
widget.field.readOnly = false;
fixture.detectChanges();
await fixture.whenStable();
let dateButton = element.querySelector<HTMLButtonElement>('button');
expect(dateButton.disabled).toBeFalsy();
widget.field.readOnly = true;
fixture.detectChanges();
await fixture.whenStable();
dateButton = element.querySelector<HTMLButtonElement>('button');
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(), {
id: 'date-field-id',
name: 'date-name',
@@ -177,42 +174,43 @@ describe('DateWidgetComponent', () => {
widget.field.readOnly = false;
fixture.detectChanges();
await fixture.whenStable();
widget.dateInputControl.setValue(new Date('invalid date'));
fixture.detectChanges();
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, {
id: 'date-field-id',
name: 'date-name',
// always stored as dd-MM-yyyy
value: '30-12-9999',
value: new Date('12-30-9999'),
type: FormFieldTypes.DATE,
readOnly: 'false',
dateDisplayFormat: 'MM-DD-YYYY'
readOnly: false,
dateDisplayFormat: 'MM-dd-yyyy'
});
widget.field = field;
fixture.detectChanges();
await fixture.whenStable();
const dateElement = element.querySelector<HTMLInputElement>('#date-field-id');
expect(dateElement).toBeDefined();
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.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', () => {
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 }), {
id: 'date-id',
name: 'date-name',
@@ -223,7 +221,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const widgetContainer = element.querySelector('.adf-left-label-input-container');
expect(widgetContainer).not.toBeNull();
@@ -235,7 +232,7 @@ describe('DateWidgetComponent', () => {
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 }), {
id: 'date-id',
name: 'date-name',
@@ -246,7 +243,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const widgetContainer = element.querySelector('.adf-left-label-input-container');
expect(widgetContainer).toBeNull();
@@ -258,7 +254,7 @@ describe('DateWidgetComponent', () => {
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' }), {
id: 'date-id',
name: 'date-name',
@@ -269,7 +265,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const widgetContainer = element.querySelector('.adf-left-label-input-container');
expect(widgetContainer).toBeNull();
@@ -291,7 +286,7 @@ describe('DateWidgetComponent', () => {
});
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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -300,7 +295,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const expectedMinDate = adapter.today();
@@ -308,7 +302,7 @@ describe('DateWidgetComponent', () => {
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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -317,13 +311,12 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
expect(widget.minDate).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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -332,7 +325,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const expectedMinDate = subDays(adapter.today(), 2);
@@ -340,7 +332,7 @@ describe('DateWidgetComponent', () => {
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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -349,7 +341,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const expectedMinDate = addDays(adapter.today(), 2);
@@ -359,7 +350,7 @@ describe('DateWidgetComponent', () => {
});
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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -368,7 +359,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const expectedMaxDate = adapter.today();
@@ -376,7 +366,7 @@ describe('DateWidgetComponent', () => {
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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -385,13 +375,12 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
expect(widget.maxDate).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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -400,7 +389,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
const expectedMaxDate = subDays(adapter.today(), 2);
@@ -408,7 +396,7 @@ describe('DateWidgetComponent', () => {
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, {
type: FormFieldTypes.DATE,
dynamicDateRangeSelection: true,
@@ -417,7 +405,6 @@ describe('DateWidgetComponent', () => {
});
fixture.detectChanges();
await fixture.whenStable();
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();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
@@ -445,9 +431,8 @@ describe('DateWidgetComponent', () => {
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();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeFalsy();
@@ -455,7 +440,6 @@ describe('DateWidgetComponent', () => {
dateCloudInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeTruthy();
});

View File

@@ -17,15 +17,42 @@
/* 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 { Subject } from 'rxjs';
import { WidgetComponent, FormService, AdfDateFnsAdapter, DateFnsUtils, ADF_DATE_FORMATS } from '@alfresco/adf-core';
import { MatDatepickerInputEvent } from '@angular/material/datepicker';
import { addDays } from 'date-fns';
import { takeUntil } from 'rxjs/operators';
import {
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({
selector: 'date-widget',
standalone: true,
imports: [
NgIf,
TranslateModule,
MatFormFieldModule,
MatInputModule,
MatDatepickerModule,
MatTooltipModule,
ReactiveFormsModule,
ErrorWidgetComponent
],
providers: [
{ provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: DateAdapter, useClass: AdfDateFnsAdapter }
@@ -47,80 +74,143 @@ import { addDays } from 'date-fns';
})
export class DateCloudWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
typeId = 'DateCloudWidgetComponent';
readonly DATE_FORMAT = 'dd-MM-yyyy';
minDate: Date = null;
maxDate: Date = null;
startAt: 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;
dateInputControl: FormControl<Date> = new FormControl<Date>(null);
private onDestroy$ = new Subject<boolean>();
private onDestroy$ = new Subject<void>();
constructor(public formService: FormService, private dateAdapter: DateAdapter<Date>) {
super(formService);
public readonly formService = inject(FormService);
private readonly dateAdapter = inject(DateAdapter);
ngOnInit(): void {
this.patchFormControl();
this.initDateAdapter();
this.initRangeSelection();
this.initStartAt();
this.subscribeToDateChanges();
this.updateField();
}
ngOnInit() {
if (this.field.dateDisplayFormat) {
updateField(): void {
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;
adapter.displayFormat = this.field.dateDisplayFormat;
}
}
if (this.field) {
if (this.field.dynamicDateRangeSelection) {
private initStartAt(): void {
if (this.field?.value) {
this.startAt = this.dateAdapter.parse(this.field.value, DEFAULT_DATE_FORMAT);
}
}
private initRangeSelection(): void {
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, this.DATE_FORMAT);
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, 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);
this.field.maxValue = DateFnsUtils.formatDate(this.maxDate, DEFAULT_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);
private setStaticRangeSelection(): void {
if (this.field?.minValue) {
this.minDate = parseISO(this.field.minValue);
}
if (this.field?.maxValue) {
this.maxDate = parseISO(this.field.maxValue);
}
}
ngOnDestroy() {
this.onDestroy$.next(true);
ngOnDestroy(): void {
this.onDestroy$.next();
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,
ContentNodeSelectorModule
} from '@alfresco/adf-content-services';
import { DateCloudWidgetComponent } from './components/widgets/date/date-cloud.widget';
import { DropdownCloudWidgetComponent } from './components/widgets/dropdown/dropdown-cloud.widget';
import { GroupCloudWidgetComponent } from './components/widgets/group/group-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,
RadioButtonsCloudWidgetComponent,
AttachFileCloudWidgetComponent,
DateCloudWidgetComponent,
PeopleCloudWidgetComponent,
GroupCloudWidgetComponent,
PropertiesViewerWrapperComponent,
@@ -96,7 +93,6 @@ import { FormCloudSpinnerService } from './services/spinner/form-cloud-spinner.s
DropdownCloudWidgetComponent,
RadioButtonsCloudWidgetComponent,
AttachFileCloudWidgetComponent,
DateCloudWidgetComponent,
PeopleCloudWidgetComponent,
GroupCloudWidgetComponent,
PropertiesViewerWidgetComponent,