Revert "Revert "AAE-23521 Fix improve dropdown reactive form"" (#10073)

* Revert "Revert "AAE-23521 Fix improve dropdown reactive form" (#10068)"

This reverts commit 554218d11e.

* AAE-23521 fix trigger error check

* AAE-23521 improve standalone import

---------

Co-authored-by: Kasia Biernat <kasia.biernat@hyland.com>
This commit is contained in:
Ehsan Rezaei
2024-09-02 14:27:10 +02:00
committed by GitHub
parent 8a60a26701
commit beae4054f2
13 changed files with 457 additions and 328 deletions

View File

@@ -15,7 +15,19 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, Output, ViewEncapsulation, SimpleChanges, OnInit, OnDestroy, OnChanges, inject } from '@angular/core';
import {
Component,
EventEmitter,
Input,
Output,
ViewEncapsulation,
SimpleChanges,
OnInit,
OnDestroy,
OnChanges,
inject,
ChangeDetectorRef
} from '@angular/core';
import {
WidgetVisibilityService,
FormService,
@@ -64,6 +76,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
protected visibilityService = inject(WidgetVisibilityService);
protected ecmModelService = inject(EcmModelService);
protected nodeService = inject(NodesApiService);
private cdRef = inject(ChangeDetectorRef);
/** Underlying form model instance. */
@Input()
@@ -131,6 +144,7 @@ export class FormComponent extends FormBaseComponent implements OnInit, OnDestro
this.formService.validateForm.pipe(takeUntil(this.onDestroy$)).subscribe((validateFormEvent) => {
if (validateFormEvent.errorsField.length > 0) {
this.formError.next(validateFormEvent.errorsField);
this.cdRef.detectChanges();
}
});
}

View File

@@ -1,23 +1,16 @@
<div class="adf-dropdown-widget {{field.className}}"
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-readonly]="field.readOnly">
<div
class="adf-dropdown-widget {{field.className}}"
[class.adf-invalid]="dropdownControl.invalid && dropdownControl.touched"
[class.adf-readonly]="field.readOnly"
>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<mat-form-field>
<mat-select class="adf-select"
[id]="field.id"
[(ngModel)]="field.value"
[disabled]="field.readOnly"
(ngModelChange)="onFieldChanged(field)"
(blur)="markAsTouched()">
<mat-option *ngFor="let opt of field.options"
[value]="getOptionValue(opt, field.value)"
[id]="opt.id">{{opt.name}}
</mat-option>
<mat-option id="readonlyOption" *ngIf="isReadOnlyType()" [value]="field.value">{{field.value}}</mat-option>
<mat-select class="adf-select" [id]="field.id" [formControl]="dropdownControl">
<mat-option *ngFor="let opt of field.options" [value]="opt" [id]="opt.id">{{opt.name}}</mat-option>
<mat-option id="readonlyOption" *ngIf="dropdownControl.disabled" [value]="field.value">{{field.value}}</mat-option>
</mat-select>
</mat-form-field>
<ng-container *ngIf="!isReadOnlyField">
<ng-container *ngIf="!isReadOnlyField && dropdownControl.touched">
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget class="adf-dropdown-required-message" *ngIf="showRequiredMessage()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</ng-container>
</div>

View File

@@ -139,9 +139,12 @@ describe('DropdownWidgetComponent', () => {
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
id: 'dropdown-id',
type: FormFieldTypes.DROPDOWN,
required: true
});
widget.ngOnInit();
});
it('should be able to display label with asterisk', async () => {
@@ -157,8 +160,9 @@ describe('DropdownWidgetComponent', () => {
it('should be invalid if no default option after interaction', async () => {
expect(element.querySelector('.adf-invalid')).toBeFalsy();
const dropdownSelect = element.querySelector('.adf-select');
dropdownSelect.dispatchEvent(new Event('blur'));
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
await dropdown.focus();
await dropdown.blur();
fixture.detectChanges();
await fixture.whenStable();
@@ -186,12 +190,14 @@ describe('DropdownWidgetComponent', () => {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
readOnly: false,
restUrl: 'fake-rest-url',
optionType: 'rest'
});
widget.field.emptyOption = { id: 'empty', name: 'Choose one...' };
widget.field.isVisible = true;
widget.ngOnInit();
fixture.detectChanges();
});
@@ -213,9 +219,8 @@ describe('DropdownWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
expect(await dropdown.getValueText()).toBe('option_2');
});
it('should select the empty value when no default is chosen', async () => {
@@ -224,8 +229,9 @@ describe('DropdownWidgetComponent', () => {
await (await loader.getHarness(MatSelectHarness)).open();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
expect(await dropdown.getValueText()).toBe('Choose one...');
expect(await widget.field.value).toBe('empty');
});
});
@@ -237,7 +243,7 @@ describe('DropdownWidgetComponent', () => {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'false',
readOnly: false,
restUrl: 'fake-rest-url',
optionType: 'rest'
});
@@ -264,9 +270,8 @@ describe('DropdownWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('option_2');
expect(dropDownElement.attributes['ng-reflect-model'].textContent).toBe('option_2');
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
expect(await dropdown.getValueText()).toBe('option_2');
});
it('should select the empty value when no default is chosen', async () => {
@@ -274,8 +279,9 @@ describe('DropdownWidgetComponent', () => {
widget.ngOnInit();
await (await loader.getHarness(MatSelectHarness)).open();
const dropDownElement: any = element.querySelector('#dropdown-id');
expect(dropDownElement.attributes['ng-reflect-model'].value).toBe('empty');
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '#dropdown-id' }));
expect(await dropdown.getValueText()).toBe('Choose one...');
expect(await widget.field.value).toBe('empty');
});
it('should be disabled when the field is readonly', async () => {
@@ -283,9 +289,10 @@ describe('DropdownWidgetComponent', () => {
id: 'dropdown-id',
name: 'date-name',
type: 'dropdown',
readOnly: 'true',
readOnly: true,
restUrl: 'fake-rest-url'
});
widget.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
@@ -304,10 +311,13 @@ describe('DropdownWidgetComponent', () => {
readOnly: true,
params: { field: { name: 'date-name', type: 'dropdown' } }
});
widget.ngOnInit();
const select = await loader.getHarness(MatSelectHarness);
fixture.detectChanges();
await fixture.whenStable();
expect(await select.getValueText()).toEqual('FakeValue');
const dropdown = await loader.getHarness(MatSelectHarness);
expect(await dropdown.getValueText()).toEqual('FakeValue');
});
});
});

View File

@@ -17,20 +17,21 @@
/* eslint-disable @angular-eslint/component-selector */
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService, FormFieldOption, WidgetComponent, ErrorWidgetComponent } from '@alfresco/adf-core';
import { Component, inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormService, FormFieldOption, WidgetComponent, ErrorWidgetComponent, ErrorMessageModel, FormFieldModel } from '@alfresco/adf-core';
import { ProcessDefinitionService } from '../../services/process-definition.service';
import { TaskFormService } from '../../services/task-form.service';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { FormsModule } from '@angular/forms';
import { AbstractControl, FormControl, ReactiveFormsModule, ValidationErrors, ValidatorFn } from '@angular/forms';
import { filter, Subject, takeUntil } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'dropdown-widget',
standalone: true,
imports: [CommonModule, TranslateModule, MatFormFieldModule, MatSelectModule, FormsModule, ErrorWidgetComponent],
imports: [CommonModule, TranslateModule, MatFormFieldModule, MatSelectModule, ReactiveFormsModule, ErrorWidgetComponent],
templateUrl: './dropdown.widget.html',
styleUrls: ['./dropdown.widget.scss'],
host: {
@@ -46,19 +47,50 @@ import { TranslateModule } from '@ngx-translate/core';
},
encapsulation: ViewEncapsulation.None
})
export class DropdownWidgetComponent extends WidgetComponent implements OnInit {
constructor(public formService: FormService, public taskFormService: TaskFormService, public processDefinitionService: ProcessDefinitionService) {
super(formService);
export class DropdownWidgetComponent extends WidgetComponent implements OnInit, OnDestroy {
public formsService = inject(FormService);
public taskFormService = inject(TaskFormService);
public processDefinitionService = inject(ProcessDefinitionService);
dropdownControl = new FormControl<FormFieldOption | string>(undefined);
private readonly onDestroy$ = new Subject<void>();
get isReadOnlyType(): boolean {
return this.field.type === 'readonly';
}
get isReadOnlyField(): boolean {
return this.field.readOnly;
}
private get isRestType(): boolean {
return this.field?.optionType === 'rest';
}
private get hasRestUrl(): boolean {
return !!this.field?.restUrl;
}
private get isValidRestConfig(): boolean {
return this.isRestType && this.hasRestUrl;
}
ngOnInit() {
if (this.isValidRestConfig() && !this.isReadOnlyForm()) {
if (this.isValidRestConfig && !this.isReadOnlyForm()) {
if (this.field.form.taskId) {
this.getValuesByTaskId();
} else {
this.getValuesByProcessDefinitionId();
}
}
this.initFormControl();
}
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete();
}
getValuesByTaskId() {
@@ -85,41 +117,84 @@ export class DropdownWidgetComponent extends WidgetComponent implements OnInit {
});
}
getOptionValue(option: FormFieldOption, fieldValue: string): string {
let optionValue: string = '';
if (option.id === 'empty' || option.name !== fieldValue) {
optionValue = option.id;
} else {
optionValue = option.name;
}
return optionValue;
}
isReadOnlyType(): boolean {
return this.field.type === 'readonly';
}
showRequiredMessage(): boolean {
return (this.isInvalidFieldRequired() || this.field.value === 'empty') && this.isTouched();
}
get isReadOnlyField(): boolean {
return this.field.readOnly;
}
private isRestType(): boolean {
return this.field?.optionType === 'rest';
}
private isReadOnlyForm(): boolean {
return !!this.field?.form?.readOnly;
}
private hasRestUrl(): boolean {
return !!this.field?.restUrl;
private initFormControl() {
if (this.field?.required) {
this.dropdownControl.addValidators([this.customRequiredValidator(this.field)]);
}
if (this.field?.readOnly || this.readOnly) {
this.dropdownControl.disable({ emitEvent: false });
}
this.dropdownControl.valueChanges
.pipe(
filter(() => !!this.field),
takeUntil(this.onDestroy$)
)
.subscribe((value) => {
this.setOptionValue(value, this.field);
this.handleErrors();
this.onFieldChanged(this.field);
});
this.dropdownControl.setValue(this.getOptionValue(this.field?.value), { emitEvent: false });
this.handleErrors();
}
private isValidRestConfig(): boolean {
return this.isRestType() && this.hasRestUrl();
private handleErrors() {
if (!this.field) {
return;
}
if (this.dropdownControl.valid) {
this.field.validationSummary = new ErrorMessageModel('');
return;
}
if (this.dropdownControl.invalid && this.dropdownControl.errors.required) {
this.field.validationSummary = new ErrorMessageModel({ message: 'FORM.FIELD.REQUIRED' });
}
}
private setOptionValue(option: string | FormFieldOption, field: FormFieldModel) {
if (typeof option === 'string') {
field.value = option;
return;
}
if (option.id === 'empty' || option.name !== field.value) {
field.value = option.id;
return;
}
field.value = option.name;
}
private getOptionValue(value?: string | FormFieldOption) {
if (this.field?.readOnly || this.readOnly) {
return value;
}
if (typeof value === 'string') {
return this.field.options.find((option) => option.id === value || option.name === value);
}
return value as FormFieldOption | undefined;
}
private customRequiredValidator(field: FormFieldModel): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const isEmptyInputValue = (value: any) => value == null || ((typeof value === 'string' || Array.isArray(value)) && value.length === 0);
const isEqualToEmptyValue = (value: any) =>
field.hasEmptyValue &&
(value === field.emptyOption.id ||
value === field.emptyOption.name ||
(value.id === field.emptyOption.id && value.name === field.emptyOption.name));
return isEmptyInputValue(control.value) || isEqualToEmptyValue(control.value) ? { required: true } : null;
};
}
}