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 b6590ecb7a..5d7233e0e8 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
@@ -36,9 +36,15 @@ 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';
+import { ReactiveFormWidget } from '../widgets/reactive-widget.interface';
declare const adf: any;
+/**
+ * Component is a wrapper for widget components.
+ * It is responsible for instantiating the correct widget component
+ * based on the field type.
+ */
@Component({
selector: 'adf-form-field',
templateUrl: './form-field.component.html',
@@ -98,7 +104,7 @@ export class FormFieldComponent implements OnInit, OnDestroy {
}
});
- if (FormFieldTypes.isReactiveType(instance?.field?.type)) {
+ if (FormFieldTypes.isReactiveWidget(instance)) {
this.updateReactiveFormControlOnFormRulesEvent(instance);
}
}
@@ -106,7 +112,7 @@ export class FormFieldComponent implements OnInit, OnDestroy {
}
}
- private updateReactiveFormControlOnFormRulesEvent(instance: any): void {
+ private updateReactiveFormControlOnFormRulesEvent(instance: ReactiveFormWidget): void {
instance?.formService.formRulesEvent.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
instance?.updateReactiveFormControl();
instance?.field?.form.validateForm(instance?.field);
diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-types.ts b/lib/core/src/lib/form/components/widgets/core/form-field-types.ts
index 6b906a9228..f6a814530b 100644
--- a/lib/core/src/lib/form/components/widgets/core/form-field-types.ts
+++ b/lib/core/src/lib/form/components/widgets/core/form-field-types.ts
@@ -15,6 +15,8 @@
* limitations under the License.
*/
+import { MaybeReactiveFormWidget, ReactiveFormWidget } from '../reactive-widget.interface';
+
/* eslint-disable @angular-eslint/component-selector */
export class FormFieldTypes {
@@ -71,6 +73,10 @@ export class FormFieldTypes {
return FormFieldTypes.REACTIVE_TYPES.includes(type);
}
+ static isReactiveWidget(instance: MaybeReactiveFormWidget): instance is ReactiveFormWidget {
+ return FormFieldTypes.REACTIVE_TYPES.includes(instance?.field?.type);
+ }
+
static isConstantValueType(type: string): boolean {
return FormFieldTypes.CONSTANT_VALUE_TYPES.includes(type);
}
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
index 5965511c65..67ef97f9d8 100644
--- a/lib/core/src/lib/form/components/widgets/reactive-widget.interface.ts
+++ b/lib/core/src/lib/form/components/widgets/reactive-widget.interface.ts
@@ -16,8 +16,13 @@
*/
import { FormService } from '../../services/form.service';
+import { FormFieldModel } from './core/form-field.model';
+import { WidgetComponent } from './widget.component';
export interface ReactiveFormWidget {
updateReactiveFormControl(): void;
formService: FormService;
+ field: FormFieldModel;
}
+
+export type MaybeReactiveFormWidget = WidgetComponent | ReactiveFormWidget;
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html
index 39c48994f0..a23f75a89e 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html
@@ -12,7 +12,8 @@
@if ( (field.name || this.field?.required) && !field.leftLabels) {
- {{ field.name | translate }} }
+ {{ field.name | translate }}
+ }
- {{opt.name}}
- {{field.value}}
+ @for(opt of (list$ | async); track opt.id) {
+
+ {{opt.name}}
+
+ }
+
+ @if(isReadOnlyType) {
+
+ {{field.value}}
+
+ }
+
{
expect(requiredErrorElement).toBeFalsy();
});
- it('should not display required error when selecting a valid option for a required dropdown', async () => {
+ it('should not display required error when selecting a valid option for a required dropdown', fakeAsync(async () => {
widget.field.required = true;
widget.field.options = [{ id: 'empty', name: 'Choose empty' }, ...fakeOptionList];
widget.ngOnInit();
+ tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
await dropdown.open();
@@ -307,7 +317,7 @@ describe('DropdownCloudWidgetComponent', () => {
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement).toBeFalsy();
- });
+ }));
it('should not have a value when switching from an available option to the None option', async () => {
widget.field.options = [{ id: 'empty', name: 'This is a mock none option' }, ...fakeOptionList];
@@ -985,48 +995,64 @@ describe('DropdownCloudWidgetComponent', () => {
expect(widget.field.options.length).toEqual(0);
};
- it('should set dropdownControl value without emitting events if the mapping is a string', () => {
+ it('should set dropdownControl value without emitting events if the mapping is a string', fakeAsync(() => {
widget.field = {
value: 'testValue',
options: [],
- isVisible: true
+ isVisible: true,
+ markAsValid: () => {}
} as any; // Mock field
+
+ fixture.detectChanges();
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
widget['setFormControlValue']();
+ tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
+
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'testValue', name: '' }, { emitEvent: false });
expect(widget.dropdownControl.value).toEqual({ id: 'testValue', name: '' });
- });
+ }));
- it('should set dropdownControl value when form field value gets changed', () => {
+ it('should set dropdownControl value when form field value gets changed', fakeAsync(() => {
widget.field = {
value: { id: 'Id_1', name: 'Label 1' },
options: [],
isVisible: true,
markAsValid: () => {}
} as FormFieldModel;
+
+ fixture.detectChanges();
+
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
widget.updateReactiveFormControl();
+ tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
+
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'Id_1', name: 'Label 1' }, { emitEvent: false });
expect(widget.dropdownControl.value).toEqual({ id: 'Id_1', name: 'Label 1' });
- });
+ }));
- it('should set dropdownControl value without emitting events if is an object', () => {
+ it('should set dropdownControl value without emitting events if is an object', fakeAsync(() => {
widget.field = {
value: { id: 'testValueObj', name: 'testValueObjName' },
options: [],
- isVisible: true
- } as any; // Mock field
+ isVisible: true,
+ markAsValid: () => {}
+ } as FormFieldModel;
+
+ fixture.detectChanges();
+
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
widget['setFormControlValue']();
+ tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
+
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'testValueObj', name: 'testValueObjName' }, { emitEvent: false });
expect(widget.dropdownControl.value).toEqual({ id: 'testValueObj', name: 'testValueObjName' });
- });
+ }));
it('should display options persisted from process variable', async () => {
widget.field = getVariableDropdownWidget(
@@ -1191,3 +1217,68 @@ describe('DropdownCloudWidgetComponent', () => {
});
});
});
+
+describe('DropdownCloudWidgetComponent instantiated by FormFieldComponent wrapper', () => {
+ let formFieldFixture: ComponentFixture;
+ let formFieldComponent: FormFieldComponent;
+ let loader: HarnessLoader;
+ let formRenderingService: FormRenderingService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [FormFieldComponent]
+ });
+
+ formFieldFixture = TestBed.createComponent(FormFieldComponent);
+ formFieldComponent = formFieldFixture.componentInstance;
+
+ loader = TestbedHarnessEnvironment.loader(formFieldFixture);
+
+ formRenderingService = TestBed.inject(FormRenderingService);
+ formRenderingService.register({
+ [FormFieldTypes.DROPDOWN]: () => DropdownCloudWidgetComponent
+ });
+ });
+
+ /* Checking if events emitted in FormFieldComponent are NOT triggering unnecessary calls to setValue in DropdownCloudWidgetComponent
+ This may result in setting wrong value in component
+ e.g. FormFieldComponent.updateReactiveFormControlOnFormRulesEvent
+ */
+ it('should set dropdown controller value only once', async () => {
+ formFieldComponent.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', readOnly: false, id: 'form-id' }), {
+ id: 'multiselect-id',
+ name: 'multiselect',
+ type: 'dropdown',
+ selectionType: 'multiple',
+ options: [
+ { id: 'option1', name: 'option1' },
+ { id: 'option2', name: 'option2' },
+ { id: 'other', name: 'other' }
+ ]
+ });
+
+ const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
+ await dropdown.open();
+
+ const dropdownCloudWidgetInstanceComponent = formFieldFixture.debugElement.query(
+ By.directive(DropdownCloudWidgetComponent)
+ ).componentInstance;
+
+ const setValueSpy = spyOn(dropdownCloudWidgetInstanceComponent.dropdownControl, 'setValue').and.callThrough();
+
+ dropdownCloudWidgetInstanceComponent.event(new Event('focusin'));
+
+ // Not using dropdown.clickOptions from harness since it need ot be awaited
+ // I want to simulate other events at the same time
+ const option1 = formFieldFixture.debugElement.query(By.css('[ng-reflect-id="option1"]'));
+ option1.triggerEventHandler('click');
+ dropdownCloudWidgetInstanceComponent.event(new Event('focusout'));
+
+ await dropdown.close();
+
+ const selectedOption = await dropdown.getValueText();
+
+ expect(selectedOption).toEqual('option1');
+ expect(setValueSpy).toHaveBeenCalledTimes(1);
+ });
+});
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 4fa9742fad..81eaf37204 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
@@ -35,8 +35,8 @@ import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { TranslatePipe } from '@ngx-translate/core';
-import { BehaviorSubject } from 'rxjs';
-import { filter, map } from 'rxjs/operators';
+import { BehaviorSubject, Subject } from 'rxjs';
+import { debounceTime, filter, map } from 'rxjs/operators';
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
import { FormCloudService } from '../../../services/form-cloud.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -48,6 +48,7 @@ export const DEFAULT_OPTION = {
name: 'Choose one...'
};
export const HIDE_FILTER_LIMIT = 5;
+export const DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE = 100;
/* eslint-disable @angular-eslint/component-selector */
@@ -91,6 +92,8 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
private readonly defaultVariableOptionLabel = 'name';
private readonly defaultVariableOptionPath = 'data';
+ private debounceSetValue = new Subject();
+
get showRequiredMessage(): boolean {
return this.dropdownControl.touched && this.dropdownControl.errors?.required && !this.isRestApiFailed && !this.variableOptionsFailed;
}
@@ -128,6 +131,26 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
}
ngOnInit() {
+ /*
+ We can have a lot of 'control.setValue' caused by form rules events
+ e.g. every time if we focusin/focusout etc. we are calling a setValue.
+ */
+ this.debounceSetValue.pipe(debounceTime(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
+ let value: Array | FormFieldOption | null | undefined;
+
+ if (Array.isArray(this.field.value)) {
+ value = this.field?.value;
+ } else if (this.field?.value && typeof this.field?.value === 'object') {
+ value = { id: this.field?.value.id, name: this.field?.value.name };
+ } else if (this.field.value === null) {
+ value = this.field.value;
+ } else {
+ value = { id: this.field?.value, name: '' };
+ }
+
+ this.dropdownControl.setValue(value, { emitEvent: false });
+ });
+
this.setupDropdown();
this.formService.onFormVariableChanged.subscribe(({ field }) => {
@@ -138,9 +161,8 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
}
updateReactiveFormControl(): void {
- if (!this.field.hasMultipleValues) {
- this.setFormControlValue();
- }
+ this.setFormControlValue();
+
this.updateFormControlState();
this.handleErrors();
}
@@ -198,15 +220,7 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
}
private setFormControlValue(): void {
- if (Array.isArray(this.field.value)) {
- this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
- } else if (this.field?.value && typeof this.field?.value === 'object') {
- this.dropdownControl.setValue({ id: this.field?.value.id, name: this.field?.value.name }, { emitEvent: false });
- } else if (this.field.value === null) {
- this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
- } else {
- this.dropdownControl.setValue({ id: this.field?.value, name: '' }, { emitEvent: false });
- }
+ this.debounceSetValue.next();
}
private updateFormControlState(): void {