mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-50099 Fix parameterized validation message interpolation (#12170)
This commit is contained in:
@@ -52,7 +52,8 @@ Contains the value and metadata for a field of a [`Form`](../../../lib/process-s
|
||||
| columns | [`ContainerColumnModel`](../../../lib/core/src/lib/form/components/widgets/core/container-column.model.ts)\[] | \[] | Column definitions for a container field |
|
||||
| rows | [`ContainerRowModel`](../../../lib/core/src/lib/form/components/widgets/core/container-row.model.ts)\[] | \[] | Row definitions for a repeatable section field |
|
||||
| emptyOption | [`FormFieldOption`](../../../lib/core/src/lib/form/components/widgets/core/form-field-option.ts) | | Dropdown menu item to use when no option is chosen |
|
||||
| validationSummary | string | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) |
|
||||
| validationSummary | [`ErrorMessageModel`](../../../lib/core/src/lib/form/components/widgets/core/error-message.model.ts) | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) |
|
||||
| validationSummaryChanges$ | Observable<[`ErrorMessageModel`](../../../lib/core/src/lib/form/components/widgets/core/error-message.model.ts)> | | Replays the current validation summary to subscribers and emits the completed summary after each validation |
|
||||
|
||||
## Details
|
||||
|
||||
|
||||
@@ -23,13 +23,14 @@ import { FormModel } from '../core/form.model';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||
import { of } from 'rxjs';
|
||||
import { firstValueFrom, of } from 'rxjs';
|
||||
import { FormService } from '../../../services/form.service';
|
||||
import { FormFieldEvent } from '../../../events/form-field.event';
|
||||
import { TranslationService } from '../../../../translation/translation.service';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||
|
||||
@@ -394,6 +395,76 @@ describe('AmountWidgetComponent - rendering', () => {
|
||||
expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
|
||||
});
|
||||
|
||||
describe('when validation runs without amount interaction', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
NOT_LESS_THAN: "Can't be less than {{ minValue }}",
|
||||
NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let amountField: FormFieldModel;
|
||||
let form: FormModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
await firstValueFrom(translateService.use('en'));
|
||||
|
||||
form = new FormModel({ taskId: '<id>' }, undefined, false, formService);
|
||||
amountField = new FormFieldModel(form, {
|
||||
id: 'amount-id',
|
||||
type: FormFieldTypes.AMOUNT,
|
||||
value: 1,
|
||||
minValue: '10'
|
||||
});
|
||||
form.fieldsCache = [amountField];
|
||||
amountField.validate();
|
||||
fixture.componentRef.setInput('field', amountField);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render updated parameters after direct revalidation', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
let errors = await formField.getTextErrors();
|
||||
expect(errors[0]).toContain("Can't be less than 10");
|
||||
|
||||
amountField.value = 10;
|
||||
amountField.minValue = '1';
|
||||
amountField.maxValue = '5';
|
||||
amountField.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
errors = await formField.getTextErrors();
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
expect(errors[0]).not.toContain('{{');
|
||||
});
|
||||
|
||||
it('should render updated parameters after sibling field revalidation', async () => {
|
||||
const siblingField = new FormFieldModel(form, {
|
||||
id: 'sibling-id',
|
||||
type: FormFieldTypes.TEXT,
|
||||
value: 'before'
|
||||
});
|
||||
form.fieldsCache = [amountField, siblingField];
|
||||
amountField.value = 10;
|
||||
amountField.minValue = '1';
|
||||
amountField.maxValue = '5';
|
||||
|
||||
siblingField.value = 'after';
|
||||
form.onFormFieldChanged(siblingField);
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
expect(errors[0]).not.toContain('{{');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form model has left labels', () => {
|
||||
it('should have left labels classes on leftLabels true', async () => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
import { filter, isObservable, Observable } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -121,7 +122,9 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
this.subscribeToFieldChanges();
|
||||
this.setInitialValues();
|
||||
this.initErrorStateMatcher();
|
||||
this.updateTranslateParameters();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +146,6 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
amountWidgetOnFocus(): void {
|
||||
@@ -163,7 +165,6 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
this.field.value = this.amountWidgetValue;
|
||||
super.onFieldChanged(this.field);
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
setInitialValues(): void {
|
||||
@@ -188,7 +189,6 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
} else if (!this.isInputInFocus) {
|
||||
this.amountWidgetValue = ev.field.value;
|
||||
}
|
||||
this.updateTranslateParameters();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,12 +208,4 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
!!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched())
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field?.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,24 +17,36 @@
|
||||
|
||||
export class ErrorMessageModel {
|
||||
message: string = '';
|
||||
attributes: Map<string, string> = null;
|
||||
attributes: Map<string, string> = new Map();
|
||||
|
||||
constructor(obj?: any) {
|
||||
this.message = obj?.message || '';
|
||||
this.attributes = obj?.attributes || new Map();
|
||||
|
||||
if (obj?.attributes) {
|
||||
this.attributes = obj.attributes;
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return !!this.message;
|
||||
}
|
||||
|
||||
getAttributesAsJsonObj() {
|
||||
const result = {};
|
||||
getAttributesAsJsonObj(): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
if (this.attributes.size > 0) {
|
||||
this.attributes.forEach((value, key) => {
|
||||
result[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidationSummaryTranslationParameters = (validationSummary?: ErrorMessageModel): Record<string, string> => {
|
||||
if (validationSummary?.isActive()) {
|
||||
return validationSummary.getAttributesAsJsonObj();
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
|
||||
import { DateFnsUtils } from '../../../../common';
|
||||
import { FormRulesEvent } from '../../../events/form-rules.event';
|
||||
import { firstValueFrom, map, Subject, take, timeout } from 'rxjs';
|
||||
import { firstValueFrom, map, skip, Subject, take, timeout } from 'rxjs';
|
||||
import { ErrorMessageModel, getValidationSummaryTranslationParameters } from './error-message.model';
|
||||
import { FormFieldTypes } from './form-field-types';
|
||||
import { RequiredFieldValidator } from './form-field-validator';
|
||||
import { MinValueFieldValidator, RequiredFieldValidator } from './form-field-validator';
|
||||
import { FormFieldModel } from './form-field.model';
|
||||
import { FormModel } from './form.model';
|
||||
|
||||
@@ -1274,6 +1275,57 @@ describe('FormFieldModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation summary changes', () => {
|
||||
const createField = (): FormFieldModel => {
|
||||
const form = new FormModel();
|
||||
form.fieldValidators = [new MinValueFieldValidator()];
|
||||
|
||||
return new FormFieldModel(form, {
|
||||
id: 'number-field',
|
||||
type: FormFieldTypes.NUMBER,
|
||||
value: 1,
|
||||
minValue: '10'
|
||||
});
|
||||
};
|
||||
|
||||
it('should replay an inactive validation summary before the first validation', async () => {
|
||||
const field = new FormFieldModel(new FormModel());
|
||||
|
||||
const validationSummary = await firstValueFrom(field.validationSummaryChanges$);
|
||||
|
||||
expect(validationSummary).toEqual(jasmine.any(ErrorMessageModel));
|
||||
expect(validationSummary.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should replay the completed validation summary when subscribing after validation', async () => {
|
||||
const field = createField();
|
||||
field.validate();
|
||||
|
||||
const validationSummary = await firstValueFrom(field.validationSummaryChanges$);
|
||||
|
||||
expect(validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
|
||||
expect(validationSummary.attributes.get('minValue')).toBe('10');
|
||||
});
|
||||
|
||||
it('should emit completed summaries when validation state changes', async () => {
|
||||
const field = createField();
|
||||
const invalidSummaryPromise = firstValueFrom(field.validationSummaryChanges$.pipe(skip(1)));
|
||||
|
||||
field.validate();
|
||||
const invalidSummary = await invalidSummaryPromise;
|
||||
|
||||
field.value = 10;
|
||||
const validSummaryPromise = firstValueFrom(field.validationSummaryChanges$.pipe(skip(1)));
|
||||
field.validate();
|
||||
const validSummary = await validSummaryPromise;
|
||||
|
||||
expect(invalidSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
|
||||
expect(invalidSummary.attributes.get('minValue')).toBe('10');
|
||||
expect(validSummary.isActive()).toBe(false);
|
||||
expect(validSummary.attributes.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail validation for readOnly required display-external-property field with null value', () => {
|
||||
const form = new FormModel();
|
||||
const field = new FormFieldModel(form, {
|
||||
@@ -2108,3 +2160,35 @@ describe('FormFieldTypes', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ErrorMessageModel', () => {
|
||||
it('should initialize empty attributes when attributes are omitted', () => {
|
||||
const errorMessage = new ErrorMessageModel();
|
||||
|
||||
expect(errorMessage.attributes).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('should retain provided attributes', () => {
|
||||
const attributes = new Map([['minValue', '10']]);
|
||||
|
||||
const errorMessage = new ErrorMessageModel({ attributes });
|
||||
|
||||
expect(errorMessage.attributes).toBe(attributes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidationSummaryTranslationParameters', () => {
|
||||
it('should return validation attributes when the summary is active', () => {
|
||||
const validationSummary = new ErrorMessageModel({
|
||||
message: 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN',
|
||||
attributes: new Map([['minValue', '10']])
|
||||
});
|
||||
|
||||
expect(getValidationSummaryTranslationParameters(validationSummary)).toEqual({ minValue: '10' });
|
||||
});
|
||||
|
||||
it('should return empty parameters when the summary is inactive or omitted', () => {
|
||||
expect(getValidationSummaryTranslationParameters(new ErrorMessageModel())).toEqual({});
|
||||
expect(getValidationSummaryTranslationParameters()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import { VariableConfig } from './form-field-variable-options';
|
||||
import { DataColumn } from '../../../../datatable/data/data-column.model';
|
||||
import { DateFnsUtils } from '../../../../common';
|
||||
import { isValid as isValidDate } from 'date-fns';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { ContainerRowModel } from './container-row.model';
|
||||
import { RepeatableSectionModel, ROW_ID_PREFIX, TEMPLATE_ROW_ID } from './repeatable-section.model';
|
||||
import { formFieldRuleHandler } from './handlers/form-field-rule.handler';
|
||||
@@ -38,6 +39,13 @@ export type FieldOptionType = 'rest' | 'manual' | 'variable';
|
||||
export type FieldSelectionType = 'single' | 'multiple';
|
||||
export type FieldAlignmentType = 'vertical' | 'horizontal';
|
||||
|
||||
interface ValidationSummaryChangesState {
|
||||
subject: ReplaySubject<ErrorMessageModel>;
|
||||
observable: Observable<ErrorMessageModel>;
|
||||
}
|
||||
|
||||
const validationSummaryChangesByField = new WeakMap<FormFieldModel, ValidationSummaryChangesState>();
|
||||
|
||||
const isJsonPrimitive = (value: unknown): value is null | string | number | boolean =>
|
||||
value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
||||
|
||||
@@ -126,7 +134,21 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
|
||||
// util members
|
||||
emptyOption: FormFieldOption;
|
||||
validationSummary: ErrorMessageModel;
|
||||
validationSummary: ErrorMessageModel = new ErrorMessageModel();
|
||||
|
||||
get validationSummaryChanges$(): Observable<ErrorMessageModel> {
|
||||
const existingState = validationSummaryChangesByField.get(this);
|
||||
if (existingState) {
|
||||
return existingState.observable;
|
||||
}
|
||||
|
||||
const subject = new ReplaySubject<ErrorMessageModel>(1);
|
||||
const observable = subject.asObservable();
|
||||
validationSummaryChangesByField.set(this, { subject, observable });
|
||||
subject.next(this.validationSummary);
|
||||
|
||||
return observable;
|
||||
}
|
||||
|
||||
get value(): any {
|
||||
return this._value;
|
||||
@@ -193,11 +215,13 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
for (const validator of validators) {
|
||||
if (!validator.validate(this)) {
|
||||
this._isValid = false;
|
||||
validationSummaryChangesByField.get(this)?.subject.next(this.validationSummary);
|
||||
return this._isValid;
|
||||
}
|
||||
}
|
||||
|
||||
this._isValid = true;
|
||||
validationSummaryChangesByField.get(this)?.subject.next(this.validationSummary);
|
||||
return this._isValid;
|
||||
}
|
||||
|
||||
@@ -242,7 +266,6 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
this.enableFractions = json.enableFractions;
|
||||
this.currency = json.currency;
|
||||
this.dateDisplayFormat = json.dateDisplayFormat || this.getDefaultDateFormat(json);
|
||||
this.validationSummary = new ErrorMessageModel();
|
||||
this.tooltip = json.tooltip || '';
|
||||
this.selectionType = json.selectionType;
|
||||
this.alignmentType = json.alignmentType;
|
||||
|
||||
@@ -22,8 +22,20 @@ import { UnitTestingUtils } from '../../../../testing';
|
||||
import { FormService } from '../../../services/form.service';
|
||||
import { FormFieldModel, FormFieldTypes, FormModel } from '../core';
|
||||
import { DecimalWidgetComponent } from './decimal.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('DecimalComponent', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
NOT_LESS_THAN: "Can't be less than {{ minValue }}",
|
||||
NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}",
|
||||
INVALID_DECIMAL_PRECISION: 'Precision {{ precision }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let loader: HarnessLoader;
|
||||
let widget: DecimalWidgetComponent;
|
||||
let fixture: ComponentFixture<DecimalWidgetComponent>;
|
||||
@@ -107,6 +119,62 @@ describe('DecimalComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
id: 'decimal-id',
|
||||
type: FormFieldTypes.DECIMAL,
|
||||
value: 1,
|
||||
minValue: 10
|
||||
});
|
||||
field.validate();
|
||||
field.form.showAllValidationErrors = true;
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum value in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be less than 10");
|
||||
});
|
||||
|
||||
it('should render the updated maximum value when programmatic revalidation fails', async () => {
|
||||
field.value = 10;
|
||||
field.minValue = '1';
|
||||
field.maxValue = '5';
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
});
|
||||
|
||||
it('should render decimal precision when programmatic revalidation fails', async () => {
|
||||
field.value = 1.234;
|
||||
field.minValue = '1';
|
||||
field.precision = 2;
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Precision 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form model has left labels', () => {
|
||||
it('should have left labels classes on leftLabels true', async () => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {
|
||||
|
||||
@@ -16,13 +16,15 @@
|
||||
*/
|
||||
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
|
||||
@Component({
|
||||
@@ -46,19 +48,21 @@ import { WidgetComponent } from '../widget.component';
|
||||
export class DecimalWidgetComponent extends WidgetComponent implements OnInit {
|
||||
errorStateMatcher: ErrorStateMatcher;
|
||||
translateParameters: Record<string, string> = {};
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
onDecimalFieldChanged(): void {
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -67,12 +71,4 @@ export class DecimalWidgetComponent extends WidgetComponent implements OnInit {
|
||||
!this.field.isValid && this.isTouched()
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('MultilineTextWidgetComponentComponent', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
AT_LEAST_LONG: 'Minimum {{ minLength }}',
|
||||
NO_LONGER_THAN: 'Maximum {{ maxLength }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let loader: HarnessLoader;
|
||||
let widget: MultilineTextWidgetComponentComponent;
|
||||
let fixture: ComponentFixture<MultilineTextWidgetComponentComponent>;
|
||||
@@ -109,6 +120,48 @@ describe('MultilineTextWidgetComponentComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
id: 'multiline-text-id',
|
||||
type: FormFieldTypes.MULTILINE_TEXT,
|
||||
value: 'text',
|
||||
minLength: 10
|
||||
});
|
||||
field.validate();
|
||||
field.form.showAllValidationErrors = true;
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum length in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Minimum 10');
|
||||
});
|
||||
|
||||
it('should render the updated maximum length when programmatic revalidation fails', async () => {
|
||||
field.value = 'too long';
|
||||
field.minLength = 1;
|
||||
field.maxLength = 5;
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Maximum 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when is required', () => {
|
||||
beforeEach(() => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { isObservable } from 'rxjs';
|
||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
|
||||
@Component({
|
||||
@@ -56,6 +57,9 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
if (this.enableCustomMessage != null) {
|
||||
if (isObservable(this.enableCustomMessage)) {
|
||||
this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
||||
@@ -73,12 +77,10 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
onMultilineTextFieldChanged(): void {
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -87,12 +89,4 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple
|
||||
!this.field.isValid && this.isTouched()
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,20 @@ import { UnitTestingUtils } from '../../../../testing';
|
||||
import { FormFieldModel, FormFieldTypes, FormModel } from '../core';
|
||||
import { NumberWidgetComponent } from './number.widget';
|
||||
import { DecimalNumberPipe } from '../../../../pipes';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('NumberWidgetComponent', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
NOT_LESS_THAN: "Can't be less than {{ minValue }}",
|
||||
NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}",
|
||||
NO_LONGER_THAN: 'Maximum length {{ maxLength }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let loader: HarnessLoader;
|
||||
let widget: NumberWidgetComponent;
|
||||
let fixture: ComponentFixture<NumberWidgetComponent>;
|
||||
@@ -175,6 +187,60 @@ describe('NumberWidgetComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
id: 'number-id',
|
||||
type: FormFieldTypes.NUMBER,
|
||||
value: 1,
|
||||
minValue: 10
|
||||
});
|
||||
field.validate();
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum value in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be less than 10");
|
||||
});
|
||||
|
||||
it('should render the updated maximum value when programmatic revalidation fails', async () => {
|
||||
field.value = 10;
|
||||
field.minValue = '1';
|
||||
field.maxValue = '5';
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
});
|
||||
|
||||
it('should render the maximum length when programmatic revalidation fails', async () => {
|
||||
field.value = 12345678901;
|
||||
field.minValue = '1';
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Maximum length 10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form model has left labels', () => {
|
||||
it('should have left labels classes on leftLabels true', async () => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -26,6 +27,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { DecimalNumberPipe } from '../../../../pipes';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
|
||||
@Component({
|
||||
@@ -53,6 +55,7 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
translateParameters: Record<string, string> = {};
|
||||
|
||||
private readonly decimalNumberPipe = inject(DecimalNumberPipe);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
ngOnInit() {
|
||||
if (this.field.readOnly) {
|
||||
@@ -61,11 +64,13 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
this.displayValue = this.field.value;
|
||||
}
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
protected onNumberChange(value: string) {
|
||||
@@ -74,7 +79,6 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -83,12 +87,4 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
!!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched())
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,20 @@ import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('TextWidgetComponent', () => {
|
||||
const form = new FormModel({ taskId: 'fake-task-id' });
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
AT_LEAST_LONG: 'Minimum {{ minLength }}',
|
||||
NO_LONGER_THAN: 'Maximum {{ maxLength }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let loader: HarnessLoader;
|
||||
let widget: TextWidgetComponent;
|
||||
@@ -64,6 +75,47 @@ describe('TextWidgetComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(form, {
|
||||
id: 'text-id',
|
||||
type: FormFieldTypes.TEXT,
|
||||
value: 'text',
|
||||
minLength: 10
|
||||
});
|
||||
field.validate();
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum length in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Minimum 10');
|
||||
});
|
||||
|
||||
it('should render the updated maximum length when programmatic revalidation fails', async () => {
|
||||
field.value = 'too long';
|
||||
field.minLength = 1;
|
||||
field.maxLength = 5;
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Maximum 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when template is ready', () => {
|
||||
describe('and no mask is configured on text element', () => {
|
||||
it('should raise ngModelChange event', async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, Directive, inject, InjectionToken, Input, TemplateRef, ViewEncapsulation } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -26,7 +27,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
import { ErrorMessageModel } from '../core/error-message.model';
|
||||
import { ErrorMessageModel, getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
|
||||
import { DEFAULT_TEXT_MAX_LENGTH } from '../core/form-field-validator';
|
||||
import { InputMaskDirective } from './text-mask.component';
|
||||
@@ -97,6 +98,9 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||
this.isMaskReversed = this.field.params['inputMaskReversed'] ? this.field.params['inputMaskReversed'] : false;
|
||||
}
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent): void {
|
||||
@@ -125,12 +129,10 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
onTextFieldChanged(): void {
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -143,14 +145,6 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
|
||||
private getLengthAfterPaste(input: HTMLInputElement, pastedValue: string): number {
|
||||
const value = input.value ?? '';
|
||||
const selectionStart = input.selectionStart ?? value.length;
|
||||
|
||||
Reference in New Issue
Block a user