Improvement/AAE-43974 Refactoring form fields (#11945)

* AAE-43974 Refactoring form fields with mat-error

* AAE-43974 Removed adf-error redundant class from all available places

* AAE-43974 Followed code flow syntax

* AAE-43974 Refactored based on review comments

* AAE-43974 Added dummy test lable

* AAE-43974 Added dummy error message for amount field

* AAE-43974 Refactored multiline widget based on reviewer's comment

* AAE-43974 Error icon style added by mixins

* AAE-43974 Missing Conditions added

* AAE-43974 Added Missing conditions

* AAE-43974 Consistent conditions added and dummy error message added

* AAE-43974 Added subscriptsizing dynamic for testing

* AAE-43974 Reverted Subscriptsizing

* AAE-43974 Added spacing between form fields in mobile view

* AAE-43974 Dummy error message changed

* AAE-43974 Changed error icon allignment for testing

* AAE-43974 Added dummy error message

* AAE-43974 Added form field gap in mobile view and dummy erro message

* AAE-43974 Refactored based on review coemments and changed the error icon allignment

* AAE-43974 Fixed the mixin path

* AAE-43974 Removed dummy error message for amount widget

* AAE-43974 Added adf-error-text class to mat-error for e2e fix

* AAE-43974 adf-error-text class added inside mat-error

* AAE-43974 Whitespace issue resolved

* AAE-43974 Whiespace issue fix

* AAE-43974 Added context pushing logic in text widget

* AAE-43974 Added margin-bottom for regression fix

* AAE-43974 Trying with different margin-bottom

* AAE-43974 Testing regression issue

* AAE-43974 Testing for regression failure

* AAE-43974 Testing Regression failures

* AAE-43974 Test Regression failure

* AAE-43974 Testing multiline Regression failure

* AAE-43974 Testing fix regarding regression failure related to multiline-text widget

* AAE-43974 Test Regression failure for multiline-text widget

* AAE-43974 Testing regression failure for multiline-text

* AAE-43974 Testing regression failure for multi-line

* AAE-43974 Testing regression failure

* AAE-43974 Testing Regression failure

* AAE-43974 Testing regression failure

* AAE-43974 Removed the commented code

* [ci:force] re-trigger CI

---------

Co-authored-by: Ehsan Rezaei <ehsan.rezaei@hyland.com>
This commit is contained in:
Anamika Dey
2026-07-08 18:32:46 +00:00
committed by GitHub
co-authored by Ehsan Rezaei
parent 23f0392c12
commit b0dc85422c
75 changed files with 756 additions and 445 deletions
@@ -84,7 +84,7 @@
/>
@if (cardViewDateTimeControl.hasError('matDatepickerParse')) {
<mat-error>
{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate }}
<span class="adf-error-text">{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate }}</span>
</mat-error>
}
</mat-form-field>
@@ -39,6 +39,10 @@
}
.adf-container-widget {
.adf-form-field-input:not(.adf-inplace-input-mat-form-field, .adf-people-cloud, .adf-cloud-group) {
margin-bottom: 35px;
}
.adf-grid-list {
display: grid;
@@ -392,8 +392,8 @@ describe('Form Renderer Component', () => {
fixture.detectChanges();
await fixture.whenStable();
expectElementToBeHidden(testingUtils, 'Number2');
const errorWidgetText = testingUtils.getByCSS('#field-Number1-container error-widget .adf-error-text').nativeElement;
expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
const errorText = testingUtils.getByCSS('#field-Number1-container .adf-error-text').nativeElement;
expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without mandatory field');
});
@@ -569,16 +569,16 @@ describe('Form Renderer Component', () => {
fixture.detectChanges();
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0x8cbv');
let errorWidgetText = testingUtils.getByCSS('#field-Number0x8cbv-container error-widget .adf-error-text').nativeElement;
expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
let errorText = testingUtils.getByCSS('#field-Number0x8cbv-container .adf-error-text').nativeElement;
expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without mandatory field');
typeIntoInput(testingUtils, '#Number0x8cbv', '?');
fixture.detectChanges();
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0x8cbv');
errorWidgetText = testingUtils.getByCSS('#field-Number0x8cbv-container error-widget .adf-error-text').nativeElement;
expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
errorText = testingUtils.getByCSS('#field-Number0x8cbv-container .adf-error-text').nativeElement;
expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without mandatory field');
typeIntoInput(testingUtils, '#Number0x8cbv', '-5');
@@ -600,8 +600,8 @@ describe('Form Renderer Component', () => {
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0him2z');
let errorWidgetText = testingUtils.getByCSS('#field-Number0him2z-container error-widget .adf-error-text').nativeElement;
expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.NOT_LESS_THAN`);
let errorText = testingUtils.getByCSS('#field-Number0him2z-container .adf-error-text').nativeElement;
expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.NOT_LESS_THAN`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without valid field');
typeIntoInput(testingUtils, '#Number0him2z', '10');
@@ -621,8 +621,8 @@ describe('Form Renderer Component', () => {
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0him2z');
errorWidgetText = testingUtils.getByCSS('#field-Number0him2z-container error-widget .adf-error-text').nativeElement;
expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
errorText = testingUtils.getByCSS('#field-Number0him2z-container .adf-error-text').nativeElement;
expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without valid field');
});
@@ -27,16 +27,17 @@
[(ngModel)]="amountWidgetValue"
(ngModelChange)="onFieldChangedAmountWidget()"
[disabled]="field.readOnly"
[errorStateMatcher]="errorStateMatcher"
(focus)="amountWidgetOnFocus()"
(blur)="amountWidgetOnBlur()"
/>
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
</mat-error>
}
</mat-form-field>
<div class="adf-error-messages-container">
<error-widget [error]="field.validationSummary" />
<error-widget
*ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
/>
</div>
</div>
</div>
@@ -1,5 +1,6 @@
/* stylelint-disable selector-class-pattern */
@use '../../../../styles/mat-selectors' as ms;
@use '../../../../styles/mixins' as mixins;
.adf {
&-amount-widget {
@@ -25,3 +26,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -335,8 +335,9 @@ describe('AmountWidgetComponent - rendering', () => {
await input.setValue('gdfgdf');
expect(widget.field.isValid).toBe(false);
const errorWidget = testingUtils.getByCSS('error-widget .adf-error-text').nativeElement;
expect(errorWidget.textContent).toBe('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
const formField = await testingUtils.getMatFormField();
const errors = await formField.getTextErrors();
expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
});
it('[C309693] - Should be possible to set the Advanced Properties for Amount Widget', async () => {
@@ -372,13 +373,14 @@ describe('AmountWidgetComponent - rendering', () => {
await input.setValue('8');
expect(widget.field.isValid).toBe(false);
let errorMessage = testingUtils.getByCSS('.adf-error-text').nativeElement;
expect(errorMessage.textContent.trim()).toContain('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
const formField = await testingUtils.getMatFormField();
let errors = await formField.getTextErrors();
expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
await input.setValue('99');
expect(widget.field.isValid).toBe(false);
errorMessage = testingUtils.getByCSS('.adf-error-text').nativeElement;
expect(errorMessage.textContent.trim()).toContain('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN');
errors = await formField.getTextErrors();
expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN');
await input.setValue('80');
expect(widget.field.isValid).toBe(true);
@@ -388,8 +390,8 @@ describe('AmountWidgetComponent - rendering', () => {
await input.setValue('incorrect format');
expect(widget.field.isValid).toBe(false);
errorMessage = testingUtils.getByCSS('.adf-error-text').nativeElement;
expect(errorMessage.textContent.trim()).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
errors = await formField.getTextErrors();
expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
});
describe('when form model has left labels', () => {
@@ -19,11 +19,12 @@
import { CurrencyPipe, NgIf } from '@angular/common';
import { Component, OnInit, ViewEncapsulation, InjectionToken, inject, DestroyRef } from '@angular/core';
import { FormsModule } from '@angular/forms';
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 { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { filter, isObservable, Observable } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -52,7 +53,7 @@ export const ADF_AMOUNT_SETTINGS = new InjectionToken<Observable<AmountWidgetSet
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent, TranslatePipe, NgIf],
imports: [MatFormFieldModule, MatInputModule, FormsModule, TranslatePipe, NgIf, MatIconModule],
providers: [CurrencyPipe],
encapsulation: ViewEncapsulation.None
})
@@ -69,11 +70,13 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
currencyDisplay: string | boolean = 'symbol';
decimalProperty: string;
enableDisplayBasedOnLocale: boolean;
errorStateMatcher: ErrorStateMatcher;
isInputInFocus = false;
locale: string;
notShowDecimalDigits = '1.0-0';
showDecimalDigits = '1.2-2';
showReadonlyPlaceholder: boolean;
translateParameters: Record<string, string> = {};
valueAsNumber: number;
get placeholder(): string {
@@ -117,6 +120,8 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
}
this.subscribeToFieldChanges();
this.setInitialValues();
this.initErrorStateMatcher();
this.updateTranslateParameters();
}
}
@@ -138,6 +143,7 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
}
}
this.markAsTouched();
this.updateTranslateParameters();
}
amountWidgetOnFocus(): void {
@@ -156,6 +162,8 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
onFieldChangedAmountWidget(): void {
this.field.value = this.amountWidgetValue;
super.onFieldChanged(this.field);
this.markAsTouched();
this.updateTranslateParameters();
}
setInitialValues(): void {
@@ -180,6 +188,7 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
} else if (!this.isInputInFocus) {
this.amountWidgetValue = ev.field.value;
}
this.updateTranslateParameters();
});
}
@@ -192,4 +201,19 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
this.enableDisplayBasedOnLocale = data?.enableDisplayBasedOnLocale ?? false;
this.showReadonlyPlaceholder = data?.showReadonlyPlaceholder;
}
private initErrorStateMatcher(): void {
this.errorStateMatcher = {
isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
!!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 = {};
}
}
}
@@ -37,9 +37,12 @@
[touchUi]="true"
[timeInterval]="5"
[disabled]="field.readOnly" />
@if (datetimeInputControl.invalid && datetimeInputControl.touched && field.validationSummary?.message) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text">{{ field.validationSummary.message | translate:translateParameters }}</span>
</mat-error>
}
</mat-form-field>
<div class="adf-error-messages-container">
<error-widget *ngIf="datetimeInputControl.invalid && datetimeInputControl.touched" [error]="field.validationSummary" />
</div>
</div>
</div>
@@ -1,5 +1,6 @@
/* stylelint-disable selector-class-pattern */
@use '../../../../styles/mat-selectors' as ms;
@use '../../../../styles/mixins' as mixins;
.adf {
&-date-time-widget {
@@ -23,6 +24,10 @@
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
#{ms.$mat-datetimepicker-toggle} {
color: var(--mat-sys-on-surface);
}
@@ -22,12 +22,12 @@ import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angul
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 { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerModule } from '@mat-datetimepicker/core';
import { TranslatePipe } from '@ngx-translate/core';
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 { ErrorMessageModel } from '../core/error-message.model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -46,13 +46,14 @@ import { ReactiveFormWidget } from '../reactive-widget.interface';
host: {
'(click)': 'event($event)'
},
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, ReactiveFormsModule, ErrorWidgetComponent],
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, ReactiveFormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class DateTimeWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget {
minDate: Date;
maxDate: Date;
datetimeInputControl: FormControl<Date> = new FormControl<Date>(null);
translateParameters: Record<string, string> = {};
public readonly formService = inject(FormService);
private readonly destroyRef = inject(DestroyRef);
@@ -116,6 +117,15 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit,
this.resetErrors();
this.field.markAsValid();
}
this.updateTranslateParameters();
}
private updateTranslateParameters(): void {
if (this.field.validationSummary?.isActive()) {
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
} else {
this.translateParameters = {};
}
}
private handleErrors(errors: ValidationErrors): void {
@@ -21,11 +21,12 @@
<mat-datepicker #datePicker
[startAt]="startAt"
[disabled]="field.readOnly" />
@if (dateInputControl.invalid && dateInputControl.touched) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}}</span>
</mat-error>
}
</mat-form-field>
<div class="adf-error-messages-container">
<error-widget
*ngIf="dateInputControl.invalid && dateInputControl.touched"
[error]="field.validationSummary"
/>
</div>
</div>
@@ -1,3 +1,5 @@
@use '../../../../styles/mixins' as mixins;
.adf {
&-widget {
&-container {
@@ -7,3 +9,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -17,17 +17,16 @@
/* eslint-disable @angular-eslint/component-selector */
import { NgIf } from '@angular/common';
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
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 { 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';
@@ -53,7 +52,7 @@ import { ReactiveFormWidget } from '../reactive-widget.interface';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [MatFormFieldModule, TranslatePipe, MatInputModule, MatDatepickerModule, ReactiveFormsModule, ErrorWidgetComponent, NgIf],
imports: [MatFormFieldModule, TranslatePipe, MatInputModule, MatDatepickerModule, ReactiveFormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class DateWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget {
@@ -118,6 +117,16 @@ export class DateWidgetComponent extends WidgetComponent implements OnInit, Reac
this.onFieldChanged(this.field);
}
get formattedMinDate(): string {
const min = this.dateInputControl.errors?.matDatepickerMin?.min;
return min ? DateFnsUtils.formatDate(min, this.field.dateDisplayFormat).toLocaleUpperCase() : '';
}
get formattedMaxDate(): string {
const max = this.dateInputControl.errors?.matDatepickerMax?.max;
return max ? DateFnsUtils.formatDate(max, this.field.dateDisplayFormat).toLocaleUpperCase() : '';
}
private validateField(): void {
if (this.dateInputControl.invalid) {
this.handleErrors(this.dateInputControl.errors);
@@ -18,19 +18,19 @@
[id]="field.id"
[required]="field.required && field.isVisible"
[(ngModel)]="field.value"
(ngModelChange)="onFieldChanged(field)"
(ngModelChange)="onDecimalFieldChanged()"
[disabled]="field.readOnly"
[placeholder]="field.placeholder"
[title]="field.tooltip"
(blur)="markAsTouched()" />
[errorStateMatcher]="errorStateMatcher"
(blur)="onBlur()" />
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
</mat-error>
}
</mat-form-field>
<div class="adf-error-messages-container">
<error-widget [error]="field.validationSummary" />
<error-widget
*ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
/>
</div>
</div>
</div>
@@ -1,3 +1,5 @@
@use '../../../../styles/mixins' as mixins;
.adf {
&-decimal-widget {
width: 100%;
@@ -9,3 +11,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -16,12 +16,13 @@
*/
import { NgIf } from '@angular/common';
import { Component, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
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 { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
@Component({
@@ -39,7 +40,39 @@ import { WidgetComponent } from '../widget.component';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent],
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class DecimalWidgetComponent extends WidgetComponent {}
export class DecimalWidgetComponent extends WidgetComponent implements OnInit {
errorStateMatcher: ErrorStateMatcher;
translateParameters: Record<string, string> = {};
ngOnInit(): void {
this.initErrorStateMatcher();
}
onBlur(): void {
this.markAsTouched();
this.updateTranslateParameters();
}
onDecimalFieldChanged(): void {
this.onFieldChanged(this.field);
this.updateTranslateParameters();
}
private initErrorStateMatcher(): void {
this.errorStateMatcher = {
isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
!this.field.isValid && this.isTouched()
};
}
private updateTranslateParameters(): void {
if (this.field.validationSummary?.isActive()) {
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
} else {
this.translateParameters = {};
}
}
}
@@ -7,6 +7,7 @@
<mat-form-field
floatPlaceholder="never"
class="adf-form-field-input"
[class.adf-has-counter]="field.maxLength > 0"
[floatLabel]="field.placeholder ? 'always' : null"
>
@if(field.name || field.required) {
@@ -20,24 +21,24 @@
rows="3"
[id]="field.id"
[required]="field.required"
[ngModel]="displayValue"
(ngModelChange)="onValueChange($event)"
[(ngModel)]="field.value"
(ngModelChange)="onMultilineTextFieldChanged()"
[disabled]="field.readOnly || readOnly"
[placeholder]="field.placeholder"
[title]="field.tooltip"
(blur)="markAsTouched()"
[errorStateMatcher]="errorStateMatcher"
(blur)="onBlur()"
>
</textarea>
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
<mat-error>
@if (field.maxLength > 0) {<span class="adf-multiline-counter-block">{{ field?.value?.length || 0 }}/{{ field.maxLength }}</span>}
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
</mat-error>
} @else if (field.maxLength > 0) {
<mat-hint class="adf-multiline-hint">{{ field?.value?.length || 0 }}/{{ field.maxLength }}</mat-hint>
}
</mat-form-field>
<div *ngIf="field.maxLength > 0" class="adf-multiline-word-counter">
<span class="adf-multiline-word-counter-value">{{ displayValue?.length || 0 }}/{{ field.maxLength }}</span>
</div>
<div class="adf-error-messages-container">
<error-widget [error]="field.validationSummary" />
<error-widget
*ngIf="isInvalidFieldRequired() && isTouched()"
class="adf-multiline-required-message"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
/>
</div>
</div>
@@ -1,9 +1,12 @@
@use '../../../../styles/mixins' as mixins;
.adf {
&-multiline-text-widget {
width: 100%;
display: flex;
align-items: flex-start;
flex-direction: column;
position: relative;
.adf-label {
top: 20px;
@@ -16,23 +19,30 @@
}
}
&-multiline-word-counter:has(.adf-multiline-word-counter-value) {
&-multiline-counter {
float: right;
margin-top: -20px;
min-height: 24px;
min-width: 1px;
font-size: var(--mat-sys-body-small-size);
color: var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6));
}
&-multiline-counter-block {
display: block;
color: var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6));
}
&-multiline-hint {
margin-top: 7px;
line-height: 14px;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2);
opacity: 1;
padding-top: 5px;
text-align: right;
padding-right: 2px;
padding-left: 0;
}
&-multiline-required-message {
display: flex;
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
.adf-container-widget .adf-multiline-text-widget .adf-form-field-input.adf-has-counter {
margin-bottom: 44px;
}
@@ -24,7 +24,6 @@ import { MultilineTextWidgetComponentComponent } from './multiline-text.widget';
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 { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
import { of, Subject } from 'rxjs';
describe('MultilineTextWidgetComponentComponent', () => {
@@ -330,92 +329,4 @@ describe('MultilineTextWidgetComponentComponent - ADF_CUSTOM_MESSAGE', () => {
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.INVALID_VALUE');
});
});
describe('typed value formatting', () => {
describe('when flag is on', () => {
beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [MultilineTextWidgetComponentComponent],
providers: [{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: true }]
});
fixture = TestBed.createComponent(MultilineTextWidgetComponentComponent);
widget = fixture.componentInstance;
testingUtils = new UnitTestingUtils(fixture.debugElement);
});
it('should return formatted name for a People value in read-only mode', () => {
widget.field = new FormFieldModel(new FormModel(), {
id: 'people-field',
type: FormFieldTypes.PEOPLE,
value: [{ firstName: 'Alice', lastName: 'Brown' }],
readOnly: true
});
fixture.detectChanges();
expect(widget.displayValue).toBe('Alice Brown');
});
it('should not return [object Object] for a complex field value', () => {
widget.field = new FormFieldModel(new FormModel(), {
id: 'people-field',
type: FormFieldTypes.PEOPLE,
value: [{ firstName: 'Alice', lastName: 'Brown' }],
readOnly: true
});
fixture.detectChanges();
expect(String(widget.displayValue)).not.toContain('[object Object]');
});
it('should pass through plain string values unchanged', () => {
widget.field = new FormFieldModel(new FormModel(), {
id: 'multiline-id',
type: FormFieldTypes.MULTILINE_TEXT,
value: 'plain text',
readOnly: true
});
fixture.detectChanges();
expect(widget.displayValue).toBe('plain text');
});
it('should not JSON-stringify a Date value for an unregistered type', () => {
const date = new Date('2026-06-02T14:30:00.000Z');
widget.field = new FormFieldModel(new FormModel(), {
id: 'date-id',
type: FormFieldTypes.MULTILINE_TEXT,
value: date,
readOnly: true
});
fixture.detectChanges();
expect(String(widget.displayValue)).toBe(String(date));
expect(String(widget.displayValue)).not.toContain('"');
});
});
describe('when flag is off', () => {
beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [MultilineTextWidgetComponentComponent]
});
fixture = TestBed.createComponent(MultilineTextWidgetComponentComponent);
widget = fixture.componentInstance;
});
it('should not format complex field values (default behaviour preserved)', () => {
widget.field = new FormFieldModel(new FormModel(), {
id: 'people-field',
type: FormFieldTypes.PEOPLE,
value: [{ firstName: 'Alice', lastName: 'Brown' }],
readOnly: true
});
fixture.detectChanges();
expect(widget.displayValue).not.toBe('Alice Brown');
});
});
});
});
@@ -17,14 +17,17 @@
/* eslint-disable @angular-eslint/component-selector */
import { NgIf } from '@angular/common';
import { Component, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
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 { ErrorWidgetComponent } from '../error/error.component';
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
import { isObservable } from 'rxjs';
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
import { WidgetComponent } from '../widget.component';
@Component({
selector: 'multiline-text-widget',
@@ -41,7 +44,55 @@ import { FormattableTextWidgetComponent } from '../core/formattable-text.widget'
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [MatFormFieldModule, NgIf, TranslatePipe, MatInputModule, FormsModule, ErrorWidgetComponent],
imports: [MatFormFieldModule, TranslatePipe, MatInputModule, FormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class MultilineTextWidgetComponentComponent extends FormattableTextWidgetComponent {}
export class MultilineTextWidgetComponentComponent extends WidgetComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly enableCustomMessage = inject(ADF_CUSTOM_MESSAGE, { optional: true });
errorStateMatcher: ErrorStateMatcher;
translateParameters: Record<string, string> = {};
ngOnInit(): void {
this.initErrorStateMatcher();
if (this.enableCustomMessage != null) {
if (isObservable(this.enableCustomMessage)) {
this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
if (this.field) {
this.field.enableCustomValidationMessage = enabled ?? false;
}
});
} else {
this.field.enableCustomValidationMessage = this.enableCustomMessage;
}
} else {
this.field.enableCustomValidationMessage = false;
}
}
onBlur(): void {
this.markAsTouched();
this.updateTranslateParameters();
}
onMultilineTextFieldChanged(): void {
this.onFieldChanged(this.field);
this.updateTranslateParameters();
}
private initErrorStateMatcher(): void {
this.errorStateMatcher = {
isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
!this.field.isValid && this.isTouched()
};
}
private updateTranslateParameters(): void {
if (this.field.validationSummary?.isActive()) {
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
} else {
this.translateParameters = {};
}
}
}
@@ -27,14 +27,15 @@
[disabled]="field.readOnly"
[placeholder]="field.placeholder"
[title]="field.tooltip"
(blur)="markAsTouched()">
[errorStateMatcher]="errorStateMatcher"
(blur)="onBlur()">
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
</mat-error>
}
</mat-form-field>
<div class="adf-error-messages-container">
<error-widget [error]="field.validationSummary" />
<error-widget
*ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
/>
</div>
</div>
</div>
@@ -1,3 +1,5 @@
@use '../../../../styles/mixins' as mixins;
.adf {
&-number-widget {
width: 100%;
@@ -9,3 +11,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -19,12 +19,13 @@
import { NgIf } from '@angular/common';
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
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 { DecimalNumberPipe } from '../../../../pipes';
import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
@Component({
@@ -42,12 +43,14 @@ import { WidgetComponent } from '../widget.component';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent],
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, MatIconModule],
providers: [DecimalNumberPipe],
encapsulation: ViewEncapsulation.None
})
export class NumberWidgetComponent extends WidgetComponent implements OnInit {
displayValue: number;
errorStateMatcher: ErrorStateMatcher;
translateParameters: Record<string, string> = {};
private readonly decimalNumberPipe = inject(DecimalNumberPipe);
@@ -57,6 +60,12 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
} else {
this.displayValue = this.field.value;
}
this.initErrorStateMatcher();
}
onBlur(): void {
this.markAsTouched();
this.updateTranslateParameters();
}
protected onNumberChange(value: string) {
@@ -65,5 +74,21 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
}
this.onFieldChanged(this.field);
this.updateTranslateParameters();
}
private initErrorStateMatcher(): void {
this.errorStateMatcher = {
isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
!!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 = {};
}
}
}
@@ -18,21 +18,23 @@
type="text"
[id]="field.id"
[required]="field.required"
[ngModel]="displayValue"
(ngModelChange)="onValueChange($event)"
[value]="field.value"
[(ngModel)]="field.value"
(ngModelChange)="onTextFieldChanged()"
[disabled]="field.readOnly || readOnly"
[textMask]="{mask: mask, isReversed: isMaskReversed}"
[placeholder]="placeholder"
[title]="field.tooltip"
(blur)="markAsTouched()">
[errorStateMatcher]="errorStateMatcher"
(blur)="onBlur()">
@if (!fieldStatusTemplate && (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched()))) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
</mat-error>
}
</mat-form-field>
<ng-container *ngTemplateOutlet="fieldStatusTemplate ?? defaultErrorMessageTemplate; context: { $implicit: this }" />
<ng-template #defaultErrorMessageTemplate>
<div class="adf-error-messages-container">
<error-widget [error]="field.validationSummary" />
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
</div>
</ng-template>
<ng-container *ngTemplateOutlet="fieldStatusTemplate ?? null; context: { $implicit: this }" />
</div>
</div>
@@ -1,3 +1,5 @@
@use '../../../../styles/mixins' as mixins;
.adf {
&-text-widget {
width: 100%;
@@ -9,3 +11,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -34,7 +34,6 @@ describe('TextWidgetComponent', () => {
let loader: HarnessLoader;
let widget: TextWidgetComponent;
let fixture: ComponentFixture<TextWidgetComponent>;
let errorWidget: HTMLElement;
let testingUtils: UnitTestingUtils;
beforeEach(() => {
@@ -126,20 +125,21 @@ describe('TextWidgetComponent', () => {
await testingUtils.fillMatInput('TEXT');
errorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
expect(errorWidget.innerHTML).toBe('FORM.FIELD.VALIDATOR.AT_LEAST_LONG');
const formField = await testingUtils.getMatFormField();
let errors = await formField.getTextErrors();
expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.AT_LEAST_LONG');
expect(widget.field.isValid).toBe(false);
await testingUtils.fillMatInput('TEXT VALUE');
errorWidget = testingUtils.getByCSS('.adf-error-text')?.nativeElement;
errors = await formField.getTextErrors();
expect(widget.field.isValid).toBe(true);
await testingUtils.fillMatInput('TEXT VALUE TOO LONG');
expect(widget.field.isValid).toBe(false);
errorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
expect(errorWidget.innerHTML).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
errors = await formField.getTextErrors();
expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
});
it('should be able to set regex pattern property for Text widget', async () => {
@@ -19,14 +19,16 @@
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, Directive, inject, InjectionToken, Input, TemplateRef, ViewEncapsulation } from '@angular/core';
import { FormsModule } from '@angular/forms';
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 { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
import { InputMaskDirective } from './text-mask.component';
import { IconModule } from '../../../../icon/icon.module';
type FieldStatusTemplate = TemplateRef<{ $implicit: WidgetComponent }>;
const FIELD_STATUS_TEMPLATE = new InjectionToken<FieldStatusTemplate>('FIELD_STATUS_TEMPLATE');
@@ -61,7 +63,7 @@ export class FieldStatusTemplateDirective {
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent, InputMaskDirective, NgTemplateOutlet],
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, InputMaskDirective, NgTemplateOutlet, IconModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class TextWidgetComponent extends FormattableTextWidgetComponent {
@@ -69,6 +71,8 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
placeholder: string;
isMaskReversed: boolean;
fieldStatusTemplate = inject(FIELD_STATUS_TEMPLATE, { optional: true });
errorStateMatcher: ErrorStateMatcher;
translateParameters: Record<string, string> = {};
override ngOnInit() {
super.ngOnInit();
@@ -81,5 +85,31 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
: this.field.placeholder;
this.isMaskReversed = this.field.params['inputMaskReversed'] ? this.field.params['inputMaskReversed'] : false;
}
this.initErrorStateMatcher();
}
private initErrorStateMatcher(): void {
this.errorStateMatcher = {
isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
!this.fieldStatusTemplate && (!!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 = {};
}
}
onBlur(): void {
this.markAsTouched();
this.updateTranslateParameters();
}
onTextFieldChanged(): void {
this.onFieldChanged(this.field);
this.updateTranslateParameters();
}
}
+8
View File
@@ -44,3 +44,11 @@
height: 100%;
overflow: hidden;
}
@mixin adf-error-icon {
font-size: 16px;
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: text-bottom;
}
@@ -10,9 +10,10 @@
type="password"
placeholder="{{ 'ADF_VIEWER.PDF_DIALOG.PLACEHOLDER' | translate }}"
[formControl]="passwordFormControl" />
@if (isError()) {
<mat-error data-automation-id='adf-password-dialog-error'><span class="adf-error-text">{{ 'ADF_VIEWER.PDF_DIALOG.ERROR' | translate }}</span></mat-error>
}
</mat-form-field>
<mat-error *ngIf="isError()" data-automation-id='adf-password-dialog-error'>{{ 'ADF_VIEWER.PDF_DIALOG.ERROR' | translate }}</mat-error>
</form>
</mat-dialog-content>
@@ -15,7 +15,6 @@
* limitations under the License.
*/
import { NgIf } from '@angular/common';
import { Component, OnInit, ViewEncapsulation, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
@@ -31,7 +30,7 @@ declare const pdfjsLib: { PasswordResponses: { NEED_PASSWORD: number; INCORRECT_
selector: 'adf-pdf-viewer-password-dialog',
templateUrl: './pdf-viewer-password-dialog.html',
styleUrls: ['./pdf-viewer-password-dialog.scss'],
imports: [MatDialogModule, IconModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule, TranslatePipe, NgIf, MatButtonModule],
imports: [MatDialogModule, IconModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule, TranslatePipe, MatButtonModule],
encapsulation: ViewEncapsulation.None
})
export class PdfPasswordDialogComponent implements OnInit {