diff --git a/lib/core/src/lib/form/components/form-field/form-field.component.spec.ts b/lib/core/src/lib/form/components/form-field/form-field.component.spec.ts index 8702076a83..b883267733 100644 --- a/lib/core/src/lib/form/components/form-field/form-field.component.spec.ts +++ b/lib/core/src/lib/form/components/form-field/form-field.component.spec.ts @@ -42,7 +42,7 @@ describe('FormFieldComponent', () => { fixture.destroy(); }); - it('should create default component instance', (done) => { + it('should create default component instance', () => { const field = new FormFieldModel(form, { type: FormFieldTypes.TEXT, id: 'FAKE-TXT-WIDGET' @@ -51,14 +51,29 @@ describe('FormFieldComponent', () => { component.field = field; fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(component.componentRef).toBeDefined(); - expect(component.componentRef.instance instanceof TextWidgetComponent).toBeTruthy(); - done(); - }); + expect(component.componentRef).toBeDefined(); + expect(component.componentRef.instance instanceof TextWidgetComponent).toBeTruthy(); }); - it('should create custom component instance', (done) => { + it('should call update form control state for reactive type widget on formRulesEvent change', () => { + const field = new FormFieldModel(form, { + type: FormFieldTypes.DATE, + id: 'FAKE-DATE-WIDGET' + }); + + component.field = field; + fixture.detectChanges(); + + const widgetInstance = component.componentRef.instance; + const updateFormControlState = spyOn(widgetInstance, 'updateReactiveFormControl'); + + widgetInstance.formService.formRulesEvent.next(); + fixture.detectChanges(); + + expect(updateFormControlState).toHaveBeenCalled(); + }); + + it('should create custom component instance', () => { formRenderingService.setComponentTypeResolver(FormFieldTypes.AMOUNT, () => CheckboxWidgetComponent, true); const field = new FormFieldModel(form, { @@ -67,16 +82,14 @@ describe('FormFieldComponent', () => { }); component.field = field; + fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(component.componentRef).toBeDefined(); - expect(component.componentRef.instance instanceof CheckboxWidgetComponent).toBeTruthy(); - done(); - }); + expect(component.componentRef).toBeDefined(); + expect(component.componentRef.instance instanceof CheckboxWidgetComponent).toBeTruthy(); }); - it('should require component type to be resolved', (done) => { + it('should require component type to be resolved', () => { const field = new FormFieldModel(form, { type: FormFieldTypes.TEXT, id: 'FAKE-TXT-WIDGET' @@ -84,16 +97,14 @@ describe('FormFieldComponent', () => { spyOn(formRenderingService, 'resolveComponentType').and.returnValue(null); component.field = field; + fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(formRenderingService.resolveComponentType).toHaveBeenCalled(); - expect(component.componentRef).toBeUndefined(); - done(); - }); + expect(formRenderingService.resolveComponentType).toHaveBeenCalled(); + expect(component.componentRef).toBeUndefined(); }); - it('should hide the field when it is not visible', (done) => { + it('should hide the field when it is not visible', () => { const field = new FormFieldModel(form, { type: FormFieldTypes.TEXT, id: 'FAKE-TXT-WIDGET' @@ -101,29 +112,26 @@ describe('FormFieldComponent', () => { component.field = field; component.field.isVisible = false; + fixture.detectChanges(); - fixture.whenStable().then(() => { - const debugElement = fixture.nativeElement.querySelector('#field-FAKE-TXT-WIDGET-container').style; - expect(debugElement.visibility).toEqual('hidden'); - expect(debugElement.display).toEqual('none'); - done(); - }); + + const debugElement = fixture.nativeElement.querySelector('#field-FAKE-TXT-WIDGET-container').style; + expect(debugElement.visibility).toEqual('hidden'); + expect(debugElement.display).toEqual('none'); }); - it('should show the field when it is visible', (done) => { + it('should show the field when it is visible', () => { const field = new FormFieldModel(form, { type: FormFieldTypes.TEXT, id: 'FAKE-TXT-WIDGET' }); component.field = field; + fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(fixture.nativeElement.querySelector('#field-FAKE-TXT-WIDGET-container').style.visibility).toEqual('visible'); - expect(fixture.nativeElement.querySelector('#field-FAKE-TXT-WIDGET-container').style.display).toEqual('block'); - done(); - }); + expect(fixture.nativeElement.querySelector('#field-FAKE-TXT-WIDGET-container').style.visibility).toEqual('visible'); + expect(fixture.nativeElement.querySelector('#field-FAKE-TXT-WIDGET-container').style.display).toEqual('block'); }); it('should hide a visible element', () => { @@ -142,7 +150,7 @@ describe('FormFieldComponent', () => { expect(fixture.nativeElement.querySelector('#field-FAKE-TXT-WIDGET-container').style.display).toEqual('none'); }); - it('[C213878] - Should fields be correctly rendered when filled with process variables', async () => { + it('[C213878] - Should fields be correctly rendered when filled with process variables', () => { const field = new FormFieldModel(form, { fieldType: 'HyperlinkRepresentation', id: 'label2', diff --git a/lib/core/src/lib/form/components/form-field/form-field.component.ts b/lib/core/src/lib/form/components/form-field/form-field.component.ts index ed84a3482c..de92de8068 100644 --- a/lib/core/src/lib/form/components/form-field/form-field.component.ts +++ b/lib/core/src/lib/form/components/form-field/form-field.component.ts @@ -20,6 +20,7 @@ import { Component, ComponentFactory, ComponentRef, + DestroyRef, inject, Input, NgModule, @@ -33,6 +34,8 @@ import { FormRenderingService } from '../../services/form-rendering.service'; import { WidgetVisibilityService } from '../../services/widget-visibility.service'; import { FormFieldModel } from '../widgets/core/form-field.model'; import { FieldStylePipe } from '../../pipes/field-style.pipe'; +import { FormFieldTypes } from '../widgets/core/form-field-types'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; declare const adf: any; @@ -62,6 +65,7 @@ export class FormFieldComponent implements OnInit, OnDestroy { private readonly formRenderingService = inject(FormRenderingService); private readonly visibilityService = inject(WidgetVisibilityService); + private readonly destroyRef = inject(DestroyRef); private readonly compiler = inject(Compiler); ngOnInit() { @@ -88,14 +92,29 @@ export class FormFieldComponent implements OnInit, OnDestroy { instance.fieldChanged.subscribe((field) => { if (field && this.field.form) { this.visibilityService.refreshVisibility(field.form); - field.form.onFormFieldChanged(field); + this.triggerFormFieldChanged(field); } }); + + if (FormFieldTypes.isReactiveType(instance?.field?.type)) { + this.updateReactiveFormControlOnFormRulesEvent(instance); + } } } } } + private updateReactiveFormControlOnFormRulesEvent(instance: any): void { + instance?.formService.formRulesEvent.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + instance?.updateReactiveFormControl(); + this.triggerFormFieldChanged(instance.field); + }); + } + + private triggerFormFieldChanged(field: FormFieldModel): void { + field.form.onFormFieldChanged(field); + } + ngOnDestroy() { if (this.componentRef) { this.componentRef.destroy(); diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts index c33e16f3d5..03b9a5458a 100644 --- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts +++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts @@ -57,15 +57,14 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { maxDate: Date; datetimeInputControl: FormControl = new FormControl(null); - public readonly formService = inject(FormService); - private readonly destroyRef = inject(DestroyRef); private readonly dateAdapter = inject(DateAdapter); private readonly dateTimeAdapter = inject(DatetimeAdapter); ngOnInit(): void { - this.patchFormControl(); + this.setFormControlValue(); + this.updateFormControlState(); this.initDateAdapter(); this.initDateRange(); this.subscribeToDateChanges(); @@ -77,12 +76,20 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit { this.onFieldChanged(this.field); } - private patchFormControl(): void { + updateReactiveFormControl(): void { + this.updateFormControlState(); + this.validateField(); + } + + private setFormControlValue(): void { this.datetimeInputControl.setValue(this.field.value, { emitEvent: false }); + } + + private updateFormControlState(): void { this.datetimeInputControl.setValidators(this.isRequired() ? [Validators.required] : []); - if (this.field?.readOnly || this.readOnly) { - this.datetimeInputControl.disable({ emitEvent: false }); - } + this.field?.readOnly || this.readOnly + ? this.datetimeInputControl.disable({ emitEvent: false }) + : this.datetimeInputControl.enable({ emitEvent: false }); this.datetimeInputControl.updateValueAndValidity({ emitEvent: false }); } diff --git a/lib/core/src/lib/form/components/widgets/date/date.widget.ts b/lib/core/src/lib/form/components/widgets/date/date.widget.ts index 52b972586d..30468ae642 100644 --- a/lib/core/src/lib/form/components/widgets/date/date.widget.ts +++ b/lib/core/src/lib/form/components/widgets/date/date.widget.ts @@ -32,6 +32,7 @@ import { WidgetComponent } from '../widget.component'; import { ErrorMessageModel } from '../core/error-message.model'; import { parseISO } from 'date-fns'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ReactiveFormWidget } from '../reactive-widget.interface'; @Component({ selector: 'date-widget', @@ -55,7 +56,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; imports: [MatFormFieldModule, TranslateModule, MatInputModule, MatDatepickerModule, ReactiveFormsModule, ErrorWidgetComponent, NgIf], encapsulation: ViewEncapsulation.None }) -export class DateWidgetComponent extends WidgetComponent implements OnInit { +export class DateWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget { minDate: Date; maxDate: Date; startAt: Date; @@ -68,7 +69,8 @@ export class DateWidgetComponent extends WidgetComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); ngOnInit(): void { - this.patchFormControl(); + this.setFormControlValue(); + this.updateFormControlState(); this.initDateAdapter(); this.initDateRange(); this.initStartAt(); @@ -80,12 +82,21 @@ export class DateWidgetComponent extends WidgetComponent implements OnInit { this.validateField(); this.onFieldChanged(this.field); } - private patchFormControl(): void { + + updateReactiveFormControl(): void { + this.updateFormControlState(); + this.validateField(); + } + + private setFormControlValue(): void { this.dateInputControl.setValue(this.field.value, { emitEvent: false }); + } + + private updateFormControlState(): void { this.dateInputControl.setValidators(this.isRequired() ? [Validators.required] : []); - if (this.field?.readOnly || this.readOnly) { - this.dateInputControl.disable({ emitEvent: false }); - } + this.field?.readOnly || this.readOnly + ? this.dateInputControl.disable({ emitEvent: false }) + : this.dateInputControl.enable({ emitEvent: false }); this.dateInputControl.updateValueAndValidity({ emitEvent: false }); } diff --git a/lib/core/src/lib/form/components/widgets/index.ts b/lib/core/src/lib/form/components/widgets/index.ts index 2a14020fc3..417902f9f7 100644 --- a/lib/core/src/lib/form/components/widgets/index.ts +++ b/lib/core/src/lib/form/components/widgets/index.ts @@ -34,6 +34,7 @@ import { DecimalWidgetComponent } from './decimal/decimal.component'; // core export * from './widget.component'; +export * from './reactive-widget.interface'; export * from './core'; // primitives diff --git a/lib/core/src/lib/form/components/widgets/reactive-widget.interface.ts b/lib/core/src/lib/form/components/widgets/reactive-widget.interface.ts new file mode 100644 index 0000000000..248b074a4d --- /dev/null +++ b/lib/core/src/lib/form/components/widgets/reactive-widget.interface.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FormService } from '../../services/form.service'; + +export interface ReactiveFormWidget { + updateReactiveFormControl(): void; + formService: FormService; +} diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts index a1313d93ff..396a1965e1 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts @@ -17,7 +17,7 @@ /* eslint-disable @angular-eslint/component-selector */ -import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, OnInit, ViewEncapsulation, DestroyRef, inject } from '@angular/core'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { ADF_DATE_FORMATS, @@ -27,7 +27,8 @@ import { ErrorMessageModel, ErrorWidgetComponent, FormService, - WidgetComponent + WidgetComponent, + ReactiveFormWidget } from '@alfresco/adf-core'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { addDays, parseISO } from 'date-fns'; @@ -61,7 +62,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }, encapsulation: ViewEncapsulation.None }) -export class DateCloudWidgetComponent extends WidgetComponent implements OnInit { +export class DateCloudWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget { typeId = 'DateCloudWidgetComponent'; minDate: Date = null; @@ -71,12 +72,13 @@ export class DateCloudWidgetComponent extends WidgetComponent implements OnInit dateInputControl: FormControl = new FormControl(null); public readonly formService = inject(FormService); - + private readonly destroyRef = inject(DestroyRef); private readonly dateAdapter = inject(DateAdapter); ngOnInit(): void { - this.patchFormControl(); + this.setFormControlValue(); + this.updateFormControlState(); this.initDateAdapter(); this.initRangeSelection(); this.initStartAt(); @@ -89,12 +91,20 @@ export class DateCloudWidgetComponent extends WidgetComponent implements OnInit this.onFieldChanged(this.field); } - private patchFormControl(): void { + updateReactiveFormControl(): void { + this.updateFormControlState(); + this.validateField(); + } + + private setFormControlValue(): void { this.dateInputControl.setValue(this.field.value, { emitEvent: false }); + } + + private updateFormControlState(): void { this.dateInputControl.setValidators(this.isRequired() ? [Validators.required] : []); - if (this.field?.readOnly || this.readOnly) { - this.dateInputControl.disable({ emitEvent: false }); - } + this.field?.readOnly || this.readOnly + ? this.dateInputControl.disable({ emitEvent: false }) + : this.dateInputControl.enable({ emitEvent: false }); this.dateInputControl.updateValueAndValidity({ emitEvent: false }); } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts index f12a170cbf..7b1fe79ce3 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts @@ -24,6 +24,7 @@ import { FormFieldOption, FormFieldTypes, FormService, + ReactiveFormWidget, RuleEntry, SelectFilterInputComponent, WidgetComponent @@ -66,10 +67,11 @@ export const HIDE_FILTER_LIMIT = 5; SelectFilterInputComponent ] }) -export class DropdownCloudWidgetComponent extends WidgetComponent implements OnInit { +export class DropdownCloudWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget { public formService = inject(FormService); private formCloudService = inject(FormCloudService); private appConfig = inject(AppConfigService); + private destroyRef = inject(DestroyRef); typeId = 'DropdownCloudWidgetComponent'; showInputFilter = false; @@ -85,7 +87,6 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI private readonly defaultVariableOptionId = 'id'; private readonly defaultVariableOptionLabel = 'name'; private readonly defaultVariableOptionPath = 'data'; - private readonly destroyRef = inject(DestroyRef); get showRequiredMessage(): boolean { return this.dropdownControl.touched && this.dropdownControl.errors?.required && !this.isRestApiFailed && !this.variableOptionsFailed; @@ -133,6 +134,11 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI }); } + updateReactiveFormControl(): void { + this.updateFormControlState(); + this.handleErrors(); + } + compareDropdownValues(opt1: FormFieldOption | string, opt2: FormFieldOption | string): boolean { if (!opt1 || !opt2) { return false; @@ -165,19 +171,14 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI this.checkFieldOptionsSource(); this.updateOptions(); - this.initFormControl(); + this.setFormControlValue(); + this.updateFormControlState(); + this.subscribeToInputChanges(); this.initFilter(); + this.handleErrors(); } - private initFormControl(): void { - if (this.field?.required) { - this.dropdownControl.addValidators([Validators.required]); - } - - if (this.field?.readOnly || this.readOnly) { - this.dropdownControl.disable({ emitEvent: false }); - } - + private subscribeToInputChanges(): void { this.dropdownControl.valueChanges .pipe( filter(() => !!this.field), @@ -188,19 +189,31 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI this.handleErrors(); this.selectionChangedForField(this.field); }); + } + private setFormControlValue(): void { this.dropdownControl.setValue(this.field?.value, { emitEvent: false }); - this.handleErrors(); + } + + private updateFormControlState(): void { + this.dropdownControl.setValidators(this.isRequired() ? [Validators.required] : []); + this.field?.readOnly || this.readOnly + ? this.dropdownControl.disable({ emitEvent: false }) + : this.dropdownControl.enable({ emitEvent: false }); + + this.dropdownControl.updateValueAndValidity({ emitEvent: false }); } private handleErrors(): void { if (this.dropdownControl.valid) { this.field.validationSummary = new ErrorMessageModel(''); + this.field.markAsValid(); return; } if (this.dropdownControl.invalid && this.dropdownControl.errors.required) { this.field.validationSummary = new ErrorMessageModel({ message: 'FORM.FIELD.REQUIRED' }); + this.field.markAsInvalid(); } } diff --git a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts index 02326416bd..f00f110ad3 100644 --- a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts +++ b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts @@ -19,12 +19,13 @@ import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core'; import { - ErrorMessageModel, - ErrorWidgetComponent, - FormFieldModel, - FormFieldOption, FormService, - WidgetComponent + FormFieldOption, + WidgetComponent, + ErrorWidgetComponent, + ErrorMessageModel, + FormFieldModel, + ReactiveFormWidget } from '@alfresco/adf-core'; import { ProcessDefinitionService } from '../../services/process-definition.service'; import { TaskFormService } from '../../services/task-form.service'; @@ -55,15 +56,14 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }, encapsulation: ViewEncapsulation.None }) -export class DropdownWidgetComponent extends WidgetComponent implements OnInit { - public formsService = inject(FormService); +export class DropdownWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget { + public formService = inject(FormService); public taskFormService = inject(TaskFormService); public processDefinitionService = inject(ProcessDefinitionService); + private readonly destroyRef = inject(DestroyRef); dropdownControl = new FormControl(undefined); - private readonly destroyRef = inject(DestroyRef); - get isReadOnlyType(): boolean { return this.field.type === 'readonly'; } @@ -93,7 +93,15 @@ export class DropdownWidgetComponent extends WidgetComponent implements OnInit { } } - this.initFormControl(); + this.setFormControlValue(); + this.updateFormControlState(); + this.subscribeToInputChanges(); + this.handleErrors(); + } + + updateReactiveFormControl(): void { + this.updateFormControlState(); + this.handleErrors(); } getValuesByTaskId() { @@ -124,15 +132,7 @@ export class DropdownWidgetComponent extends WidgetComponent implements OnInit { return !!this.field?.form?.readOnly; } - private initFormControl() { - if (this.field?.required) { - this.dropdownControl.addValidators([this.customRequiredValidator(this.field)]); - } - - if (this.field?.readOnly || this.readOnly) { - this.dropdownControl.disable({ emitEvent: false }); - } - + private subscribeToInputChanges(): void { this.dropdownControl.valueChanges .pipe( filter(() => !!this.field), @@ -143,9 +143,19 @@ export class DropdownWidgetComponent extends WidgetComponent implements OnInit { this.handleErrors(); this.onFieldChanged(this.field); }); + } + private setFormControlValue(): void { this.dropdownControl.setValue(this.getOptionValue(this.field?.value), { emitEvent: false }); - this.handleErrors(); + } + + private updateFormControlState(): void { + this.dropdownControl.setValidators(this.isRequired() ? [this.customRequiredValidator(this.field)] : []); + this.field?.readOnly || this.readOnly + ? this.dropdownControl.disable({ emitEvent: false }) + : this.dropdownControl.enable({ emitEvent: false }); + + this.dropdownControl.updateValueAndValidity({ emitEvent: false }); } private handleErrors() { @@ -155,11 +165,13 @@ export class DropdownWidgetComponent extends WidgetComponent implements OnInit { if (this.dropdownControl.valid) { this.field.validationSummary = new ErrorMessageModel(''); + this.field.markAsValid(); return; } if (this.dropdownControl.invalid && this.dropdownControl.errors.required) { this.field.validationSummary = new ErrorMessageModel({ message: 'FORM.FIELD.REQUIRED' }); + this.field.markAsInvalid(); } }