arraysangularangular-abstract-control

Angular FormControl's material-checkboxes.component has selected values but this.controlValue is an empty array


I have an Angular form I've built that consists of a single material-checkboxes component. I have two copies of this component, one is static and one is dynamic. The only difference is that the dynamic version gets its control values from an API call. Both of these examples have one or more options defaulted as checked when the controls initialize.

The issue I have is that the dynamic one's model is out of sync with its view as long as its left unchanged (ie, if I don't click on any of the checkbox controls to select or unselect them). Once I click on one of the checkboxes, the model updates to sync with the view.

I can tell this because I can submit the static version and get expected results (the defaulted items are posted as values as expected). However, when I submit the dynamic one, I get an empty post.

Here is what the component looks like with the defaulted values before I submit it to see the submitted form data:

enter image description here

And here is the resulted submitted values (as expected):

enter image description here

By way of comparison, here is the same control (material-checkboxes.component.ts) but built using an external datasource to feed in the titleMap and also has defaulted values.

enter image description here

And here is the result after submit of the above form:

enter image description here

So, as the screencaps indicate, The manually created one works as expected and submits the form containing the defaulted values. However, the component with the dynamically generated values, even though the view shows it to have selected default options, submits as EMPTY.

Expected: this.controlValue = ['12', 'd4']

Actual:

onInit > this.controlValue = ['12', 'd4']

After updateValue method > this.controlValue = undefined // But the view is unchanged from the init

However, I can get it to submit data as expected, if I manually change any of the values, even if i set them exactly as they were defaulted. Its as if the form data is not being set until manually clicking on the options.

Here is a snippet from the template that holds the component:

<mat-checkbox
    type="checkbox"
    [class.mat-checkboxes-invalid]="showError && touched"
    [class.mat-checkbox-readonly]="options?.readonly"
    [checked]="allChecked"
    [disabled]="(controlDisabled$ | async) || options?.readonly"
    [color]="options?.color || 'primary'"
    [indeterminate]="someChecked"
    [name]="options?.name"
    (focusout)="onFocusOut()"
    (change)="updateAllValues($event)"
    [required]="required"
    [value]="controlValue">

Solution

  • Update: I found that the issue was that the form control's value is not updated before leaving the syncCurrentValues() method called just after the setTitleMap hostlistener. Adding a call to this.updateValue() in syncCurrentValues() resolves it and the model and view are back in sync. However, there is a problem, but first, here is the code that resolves the issue when there is a default value set in the this.options data:

    @HostListener('document:setTitleMap', ['$event'])
      setTitleMap(event: CustomEvent) {
        if (event.detail.eventName === this.options.wruxDynamicHook && isRequester(this.componentId, event.detail.params)) {
          this.checkboxList = buildTitleMap(event.detail.titleMap, this.options.enum, true, true, this.options.allowUnselect || false);
          // Data coming in after ngInit. So if this is the first time the list is provided, then use the defaultValues from the options.
          const value = this.setDefaultValueComplete ?
            this.jsf.getFormControl(this)?.value || [] :
            [].concat(this.options?.defaultValue || []);
          this.syncCurrentValues(value);
          // Set flag to true so we ignore future calls and not overwrite potential user edits
          this.setDefaultValueComplete = true;
        }
      }
    
    updateValue(event: any = {}) {
      this.options.showErrors = true;
      // this.jsf.updateArrayCheckboxList(this, this.options.readonly ? this.checkboxListInitValues : this.checkboxList);
      this.jsf.updateArrayCheckboxList(this, this.checkboxList);
      this.onCustomAction(this.checkboxList);
      this.onCustomEvent(this.checkboxList);
      this.jsf.forceUpdates();
      if (this.jsf.mode === 'builder-properties') {
        this.jsf.elementBlurred();
      }
    }
    
    syncCurrentValues(newValues: Array<any>): void {
      for (const checkboxItem of this.checkboxList) {
        checkboxItem.checked = newValues.includes(checkboxItem.value);
      }
      this.updateValue(); // Fixed it. Otherwise, the checked items in titlemap never get synced to the model
    }
    

    The call to updateData() above fixes the issue in that case. However, when there are no default values in the options data and the checkbox data is loaded externally from an API call that executes after the ngOnInit has fired, I have the same issue. this.controlValue is empty after ngOnInit despite that the view has updated to show checked checkboxes. The model has made that happen through the setTitleMap() method but the controlValue still logs as an empty array.