Reputation: 53
I am in a team that is developing an enterprise-level full-stack web application using Spring-boot and Angular v18. The website will have multiple tables and each table will have its respective add/edit form. The configuration for each form is acquired by sending a request to the backend REST API. Then based on the configuration the forms are rendered via a form-component
in the frontend. Some forms might have dropdowns whose options are get via a dropdown service which itself uses WritableSignals
to acquire the options by again doing a call to the backend.
The issue I am facing right now is that only for the first time when I open the form the options are not getting patched to the respective dropdown. If I close the form and open it back again immediately then everything seems to be working. From debugging the code what I can understand is that the field is getting rendered before the dropdown service can acquire the options, but I am not sure how to move forward with solving this issue.
dropdown-helper.service.ts:
@Injectable({
providedIn: 'root'
})
export class DropdownHelperService {
dropdownOptionsMap = new Map<string, WritableSignal<Option[]>>();
constructor(private dataService: DataService) {
environment.tableDetails.map(table => {
return table.tableDatabaseName;
}).forEach(table => {
this.dropdownOptionsMap.set(table, signal<Option[]>([]))
});
}
loadDropdownOptions(tableName: string) {
this.dataService.getDropdownSuggestions(tableName).subscribe((dropdownOptions: any[]) => {
const options: Option[] = dropdownOptions.map(option => ({
value: option.id,
label: option.values.join(' / ')
}));
const signalRef = this.dropdownOptionsMap.get(tableName);
if(signalRef) {
signalRef.set(options);
}
}
}
}
form.component.ts:
@Component({
selector: 'app-form',
standalone: true,
imports: [
/* various imports */
],
templateUrl: './form.component.html',
styleUrl: './form.component.css',
providers: [provideNativeDateAdapter()]
})
export class FormComponent implements OnChanges {
@Input('getFormRequestData') getFormRequestData: FormComponentDataModel;
@Input() fieldsArray: FormField[];
dataForm: FormGroup;
formArray: FormArray;
filteredOptions: Map<string, Observable<Option[]>>;
constructor(private formBuilder: FormBuilder, public dropdownHelper: DropdownHelperService) {
this.getFormRequestData = {
tableName: '',
mode: '',
data: []
}
this.dataForm = this.formBuilder.group({
forms: this.formBuilder.array([])
});
this.formArray = this.dataForm.get('forms') as FormArray;
this.fieldsArray = [];
this.filteredOptions = new Map<string, Observable<Option[]>>();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['fieldsArray'] && !changes['fieldsArray'].firstChange && this.fieldsArray?.length > 0) {
this.addForm();
}
}
addForm() {
if (this.formArray.length > 0) {
this.formArray.clear();
}
const {mode, data} = this.getFormRequestData;
if (mode === 'edit' && data.length > 0) {
for (const initialData of data) {
this.formArray.push(this.createFormGroup(initialData));
}
} else if (mode === 'add' && data.length > 0 && data[0]['numberOfRowsToBeAdded'] !== 0) {
for (let i = 0; i < data[0]['numberOfRowsToBeAdded']; i++) {
this.formArray.push(this.createFormGroup());
}
}
}
createFormGroup(initialData?: any) {
const group = this.formBuilder.group({});
this.fieldsArray.forEach(field => {
if (field.controllerName !== null && field.controllerName !== undefined) {
const controller = new FormControl();
this.setupFormControl(controller, field);
if (field.type === 'select' && field.selectOptions.dropdownTable !== null) {
// setting up the options for each dropdown
this.setupDropdown(field, controller);
}
if (this.getFormRequestData.mode === 'edit' && initialData && (initialData[field.controllerName] !== null || initialData[field.controllerName] !== '')) {
this.patchControlValue(field, controller, initialData[field.controllerName]);
}
controller.updateValueAndValidity();
group.addControl(field.controllerName, controller);
}
});
return group;
}
setupFormControl(control: FormControl, field: FormField) {
if (field.validators.validators !== null && field.validators.validators !== undefined) {
const validatorFns: ValidatorFn[] = [];
field.validators.validators.forEach(validator => {
switch (validator.type) {
case 'required':
validatorFns.push(Validators.required);
break;
case 'maxLength':
validatorFns.push(Validators.maxLength(Number(validator.arg)));
break;
case 'minLength':
validatorFns.push(Validators.minLength(Number(validator.arg)));
break;
case 'exactDigitsValidator':
validatorFns.push(exactDigitsValidator(Number(validator.arg)));
break;
case 'requiredNonWhitespaceValidator':
validatorFns.push(requiredNonWhitespaceValidator())
break;
}
});
if (validatorFns.length > 0) {
control.setValidators(validatorFns);
}
}
field.disabled ? control.disable() : control.enable();
}
patchControlValue(field: FormField, control: FormControl, data: any) {
let controlData = data;
if (field.inputType === 'date') {
controlData = new Date(data);
}
if (field.type === 'select') {
this.filteredOptions.get(field.controllerName)?.subscribe(options => {
const selectedOption = options.find(option => option.value === data);
if (selectedOption) {
controlData = selectedOption.value;
}
});
}
control.setValue(controlData);
}
setupDropdown(field: FormField, control: FormControl) {
let filterColumns: string[] = [];
let filterOptions: string[] = [];
let column = field.selectOptions.filterColumns.columns;
if (column !== null && column.length > 0) {
column.forEach(c => {
filterColumns.push(c.columnName);
filterOptions.push(c.filter);
})
}
// populating the dropdownmap in the dropdown service
this.dropdownHelper.loadDropdownOptions(field.selectOptions.dropdownTable, filterColumns, filterOptions);
this.filteredOptions.set(
field.controllerName,
control.valueChanges.pipe(
startWith(''),
map(value => {
const filterValue = typeof value === 'string' ? value.toLowerCase() : '';
return this.dropdownHelper.dropdownOptionsMap
.get(field.selectOptions.dropdownTable)!()
.filter(option =>
option.label.toLowerCase().includes(filterValue)
);
})
))
}
}
form.component.html:
<form [formGroup]="dataForm">
<div formArrayName="forms">
@for (form of getForms(); track form; let i = $index) {
@if (i > 0) {
<mat-divider class="m-5"></mat-divider>
}
<div [formGroupName]="i">
@for (field of fieldsArray; track $index) {
@if (field.display) {
@switch (field.type) {
<!-- different cases for different field types -->
@case ('select') {
<mat-form-field class="w-50 pb-3 pe-2" appearance="outline">
<mat-label>{{ field.displayName }}</mat-label>
<input type="text"
placeholder="Pick one"
matInput
[formControlName]="field.controllerName"
[matAutocomplete]="auto">
<mat-error
*ngIf="isFormControlInvalid(field.controllerName, i) && form.get(field.controllerName)?.hasError('required')">
This field is required
</mat-error>
<mat-icon matSuffix>arrow_drop_down</mat-icon>
<mat-autocomplete #auto="matAutocomplete"
[displayWith]="displayFn(filteredOptions.get(field.controllerName)! | async)">
@for (option of filteredOptions.get(field.controllerName)! | async; track option) {
<mat-option [value]="option.value">{{ option.label }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
}
}
}
}
</div>
}
</div>
</form>
Upvotes: 0
Views: 23