mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
AAE-23521 Fix improve dropdown reactive form (#10040)
* AAE-23521 Fix improve dropdown reactive form * AAE-23521 Update process services dropdown * AAE-23521 pr suggestions * AAE-23521 move tests * AAE-23521 fixes
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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 { TranslateModule } from '@ngx-translate/core';
|
||||
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';
|
||||
|
||||
@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 { FormsModule } from '@angular/forms';
|
||||
},
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user