Neel
Neel

Reputation: 53

How can I patch values to mat-autocomplete only after I have gotten the values via WritableSignals?

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

Answers (0)

Related Questions