[FIXED] p-multiselect in a reactive form

Issue

I have a form that is generated from a json file. I do receive a json that looks this way :

{
  "lang": "en",
  "name": "user-opinion",
  "localName": "My opinion",
  "controls": [
    {
      "name": "hobbies",
      "label": "My hobbies",
      "value": "",
      "type": "select",
      "selected": "Select your favorite hobby",
      "multi": true,
      "selectOptions": [
        { "key": "tennis", "value": "Tennis" },
        { "key": "golf", "value": "Golf" },
        { "key": "bike", "value": "VĂ©lo"}
      ],
      "validators": {}
    },
    {
      "name": "softwares",
      "label": "Logiciels",
      "value": "",
      "type": "select",
      "selected": "Choisissez vos IDE",
      "multi": true,
      "selectOptions": [
        { "key": "eclipse", "value": "Eclipse" },
        { "key": "intellij", "value": "Intellij" },
        { "key": "webstorm", "value": "Webstorm"}
      ],
      "validators": {}
    },
    {
      "name": "comments",
      "label": "Comments",
      "value": "",
      "type": "textarea",
      "validators": {}
    },
    {
      "name": "chips",
      "label": "Chips",
      "values": [],
      "type": "chips",
      "validators": {}
    },
    {
      "name": "rating",
      "label": "Rating",
      "rating": 0,
      "stars": 5,
      "type": "rating",
      "validators": {}
    },
    {
      "name": "countries",
      "label": "Pays",
      "value": "",
      "type": "cascadeSelect",
      "cascadeSelectOptions": [
        {
          "name": "Australie",
          "code": "AU",
          "lvl1elt": [
            {
              "name": "Nouvelles Galles du Sud",
              "lvl2elt": [
                {"cname": "Sydney", "code": "A-SY"},
                {"cname": "Newcastle", "code": "A-NE"},
                {"cname": "Wollongong", "code": "A-WO"}
              ]
            },
            {
              "name": "Queensland",
              "lvl2elt": [
                {"cname": "Brisbane", "code": "A-BR"},
                {"cname": "Townsville", "code": "A-TO"}
              ]
            }
          ]
        },
        {
          "name": "Canada",
          "code": "CA",
          "lvl1elt": [
            {
              "name": "Quebec",
              "lvl2elt": [
                {"cname": "Montreal", "code": "C-MO"},
                {"cname": "Quebec City", "code": "C-QU"}
              ]
            },
            {
              "name": "Ontario",
              "lvl2elt": [
                {"cname": "Ottawa", "code": "C-OT"},
                {"cname": "Toronto", "code": "C-TO"}
              ]
            }
          ]
        },
        {
          "name": "Etats Unis",
          "code": "US",
          "lvl1elt": [
            {
              "name": "Californie",
              "lvl2elt": [
                {"cname": "Los Angeles", "code": "US-LA"},
                {"cname": "San Diego", "code": "US-SD"},
                {"cname": "San Francisco", "code": "US-SF"}
              ]
            },
            {
              "name": "Floride",
              "lvl2elt": [
                {"cname": "Jacksonville", "code": "US-JA"},
                {"cname": "Miami", "code": "US-MI"},
                {"cname": "Tampa", "code": "US-TA"},
                {"cname": "Orlando", "code": "US-OR"}
              ]
            },
            {
              "name": "Texas",
              "lvl2elt": [
                {"cname": "Austin", "code": "US-AU"},
                {"cname": "Dallas", "code": "US-DA"},
                {"cname": "Houston", "code": "US-HO"}
              ]
            }
          ]
        }
      ],
      "validators": {}
    }
  ]
}

This json is obtained with an API and uses URL to retrieve it :

ngOnInit(): void {
  // loading json response from back
  // console.log(this.router.url);
  let currentRoute = this.router.url;
  this.apiService.getTranslatedForm(currentRoute).subscribe(
      (response: any) => {
        this.jsonResponse = response
        this.buildForm((this.jsonResponse.controls))
    },
      (error: any) => {
        console.log(error)
    },
    () => {
        console.log("Done");
    }
)

}

Each element of the form are

buildForm(controls: JsonFormControls[]): void {
  // we will loop all entries of JsonFormControls objects from the controls array
  console.log("controls", controls);
  let repeatedInputFormGroup = this.fb.group({});
  for (const control of controls) {
    // some inputs have one or more validators: input can be required, have a min length, x length...
    const controlValidators = [];
    // a control has a key and a value.
    // example: "validators": { "required": true, "minLength": 10 }
    // this snippet is reusable: can be optimized if used in many forms
    for (const [key, value] of Object.entries(control.validators)) {
      switch (key) {
        case 'min':
          controlValidators.push(Validators.min(value));
          break;
        case 'max':
          controlValidators.push(Validators.max(value));
          break;
        case 'required':
          if (value) {
            controlValidators.push(Validators.required);
          }
          break;
        case 'requiredTrue':
          if (value) {
            controlValidators.push(Validators.requiredTrue);
          }
          break;
        case 'email':
          if (value) {
            controlValidators.push(Validators.email);
          }
          break;
        case 'minLength':
          controlValidators.push(Validators.minLength(value));
          break;
        case 'maxLength':
          controlValidators.push(Validators.maxLength(value));
          break;
        case 'pattern':
          controlValidators.push(Validators.pattern(value));
          break;
        case 'nullValidator':
          if (value) {
            controlValidators.push(Validators.nullValidator);
          }
          break;
        default:
          break;
      }
    }

    // we must handle repeated inputs
    const formControl = this.fb.control(control.value, controlValidators);
    if (control.repeat) {
      this.form.addControl(control.name, this.fb.array([formControl]));
    } else {
      this.form.addControl(control.name, formControl);
    }
  }
}

For this json I do make an API call to retrieve the JSON. I loop through the JSON object in order to display the form elements.

<!-- creating the form and loop -->
<span *ngIf="form != null">
  <h3>{{ jsonResponse.localName }}</h3>
  <form [formGroup]="form" (ngSubmit)="onSubmit()">
    <div *ngFor="let control of jsonResponse.controls">
      <div class="mb-3">
        <span *ngIf="control.label != '' && control.type !== 'toggle' && control.type !== 'checkbox'">
          <label class="form-label">{{ control.label }}</label>
        </span>
        <!-- for inputs that are not repeatable -->
        <span *ngIf="inputTypes.includes(control.type) && control.repeat === false">
          <input
            [type]="control.type"
            formControlName="{{control.name}}"
            [value]="control.value"
            class="form-control"
          />
        </span>
        <!-- for inputs that are repeatable -->
        <span *ngIf="inputTypes.includes(control.type) && control.repeat === true">
          <div formArrayName="{{ control.name }}">
            <div
              *ngFor="let item of getControls(control.name); let id = index"
              class="input-group mb-3">
              <input class="form-control" formControlName="{{ id }}"/>
              <button
                type="button"
                class="btn btn-outline-secondary"
                (click)="deleteInputItem(control.name, id)">
                Remove
              </button>
            </div>
            <button
              type="button"
              class="btn btn-primary"
              (click)="addInputItem(control.name)">
              Add entry
            </button>
          </div>
        </span>
        <!-- text area -->
        <span *ngIf="control.type === 'textarea'">
          <textarea
            [formControlName]="control.name"
            [value]="control.value"
            class="form-control"
          ></textarea>
        </span>
        <!-- select -->
        <span *ngIf="control.type === 'select' && control.multi == false">
          <select
            [formControlName]="control.name"
            class="form-select form-select-lg mb-3">
            <option selected>{{ control.selected }}</option>
            <option
              *ngFor="let option of control.selectOptions"
              value="{{ option.key }}"> {{ option.value }}
            </option>
          </select>
        </span>
        <!--select multi -->
        <span *ngIf="control.type === 'select' && control.multi == true">
<!--          <p>{{ control | json }}</p>-->
<!--          {{ control.selectOptions | json }}-->
          <p-multiSelect
            [formControlName]="control.name"
            [options]="control.selectOptions"
            [(ngModel)]="selection"
            optionLabel="value"></p-multiSelect>
        </span>

        <!-- cascade select -->
        <span *ngIf="control.type === 'cascadeSelect'">
          <!-- the optionGroupChildren depends on object passed in control.cascadeSelectOptions -->
          <p-cascadeSelect [options]="control.cascadeSelectOptions"
                           formControlName = "{{ control.name }}"
                           optionLabel="cname"
                           optionGroupLabel="name"
                           [optionGroupChildren]="['lvl1elt', 'lvl2elt']"
                           [style]="{'minWidth': '14rem', 'maxWidth': '100%'}"
                           placeholder="Select a City"
          >
            <ng-template pTemplate="option" let-option>
              <div class="country-item">
                <img *ngIf="option.states"/>
                <i class="pi pi-compass p-mr-2" *ngIf="option.lvl2elt"></i>
                <i class="pi pi-map-marker p-mr-2" *ngIf="option.cname"></i>
                <span>{{option.cname || option.name}}</span>
              </div>
            </ng-template>
        </p-cascadeSelect>
        </span>
        <!-- chips -->
        <span *ngIf="control.type === 'chips'">
          <p-chips
            [(ngModel)]="control.values"
            formControlName = "{{ control.name }}"
          >
          </p-chips>
        </span>

        <!-- range -->
        <span *ngIf="control.type === 'range'">
          <p-slider
            formControlName = "{{ control.name }}">
          </p-slider>
        </span>

        <span *ngIf="control.type === 'rangeSlide'">
          <p-slider
            formControlName = "{{ control.name }}"
            [min] = "getValue(control.options.min)"
            [max] ="getValue(control.options.max)"
            [step] ="getValue(control.options.step)"
            [range]="true"
          >
          </p-slider>
        </span>

        <!-- handling checkboxes -->
        <span *ngIf="control.type === 'checkbox'">
          <p-checkbox [formControlName]="control.name"
                      value="{{ control.name }}"></p-checkbox>
          <label class="form-check-label">&nbsp;{{ control.label }}</label>
        </span>
        <!-- toggle -->
        <span *ngIf="control.type === 'toggle'">
          <p-inputSwitch
            [formControlName]="control.name">
          </p-inputSwitch>
        </span>
        <!-- rating -->
        <span *ngIf="control.type === 'rating'">
          <p-rating
            [cancel]="false"
            [formControlName]="control.name"></p-rating>
        </span>

        <!-- radio buttons -->
        <span *ngIf="control.type === 'radio'">
          <div class="form-check form-check-inline" *ngFor="let option of control.radioOptions">
            <input class="form-check-input"
                   type="radio" name="{{control.name }}"
                   id="{{option.key}}-{{option.value}}"
                   [formControlName]="control.name"
                   value="{{option.value}}">
            <label class="form-check-label" for="{{option.key}}-{{option.value}}">{{option.value}}</label>
          </div>
        </span>
      </div>
    </div>
    <div>
      <button
        class="btn btn-primary"
        type="submit">
        Submit
      </button>
    </div>
  </form>
</span>
<span *ngIf="form != null">
  {{ this.form.value | json }}
</span>

I can use many different input (text, textarea…) and primeng features such as p-multiselect;

  <span *ngIf="control.type === 'select' && control.multi == true">
    <p>{{ control | json }}</p>
    {{ control.selectOptions | json }}
    <p-multiSelect
      [formControlName]="control.name"
      [options]="control.selectOptions"
      [(ngModel)]="selection"
      optionLabel="value"></p-multiSelect>
  </span>
</div>

At this stage my different form elements are displayed dynamically. I do have the options for each input. My issue is that for test purpose, I also display the values from my form:

<span *ngIf="form != null">
  {{ this.form.value | json }}
</span>

I do obtain for both inputs the values i put for the other. In hobbies, I do not have "Eclipse" value but it is set in hobbies form value for both inputs.

enter image description here

Solution

A whole day issue solved by accident.

<span *ngIf="control.type === 'select' && control.multi == true">
  <p-multiSelect
    [formControlName]="control.name"
    [options]="control.selectOptions"
    optionLabel="value">
  </p-multiSelect>
</span>

My selection in both multiselect were added to an array with using ngModel. I simply removed this stuff : [(ngModel)]="selection" and now it works like a charm. I’ll just have to clean up my form when i’ll send it to database.

enter image description here

Answered By – davidvera

Answer Checked By – Robin (Easybugfix Admin)

Leave a Reply

(*) Required, Your email will not be published