Angular 4 - Reactive Forms - select item in a list from object not referenced in this list - trackby issue?

Adrien picture Adrien · Jul 28, 2017 · Viewed 24.4k times · Source

I am converting Angular 1.6 code to Angular 4 and I have an issue with a list of elements. The code in Angular 1.6 is:

<select ng-model="$ctrl.level" ng-options="item as item.label for item in $ctrl.referentiel.levels | orderBy : 'index' track by item.id"                                   id="childLevel" name="childLevel" class="size-xl"                               >
<option value="">Select</option>
</select>

The object level is not referenced in my list because this list is loaded using the object referentiel.levels. But the matching between the elements of my list and my object Level is done thanks to trackby. So when my object Level is loaded, the element is selected in the list.

Now, I try to convert this code using Reactive Forms. In my HTML code, I have:

<select formControlName="levelControl" id="levelSelect" name="levelSelect" class="size-xl">
<option [ngValue]="null">Select</option>
<option *ngFor="let level of referentiel.levels;trackBy:identify" [ngValue]="level">{{level.label }}</option>
</select>

And in my component, I have in the OnInit method:

(<FormControl>this.myForm.controls.levelControl).setValue(this.level);

And the method identify is simple:

identify(index,item){
   return item.id;
}

But the comportment is different. When I set the value of my control using my object Level, the item with the same id in the list is not selected.

I found a solution but I don't understand why it is not working. My workaround is to write this code in HTML:

<option *ngFor="let level of referentiel.levels;trackBy:identify" [ngValue]="level.id">{{level.label }}</option>

And in my typescript file:

(<FormControl>this.myForm.controls.levelControl).setValue(this.level.id);

So, now it is working: my item is selected in the list.

I don't understand the difference between the two versions of Angular in this case. Maybe I missed something...

Thanks for your help.

Answer

AT82 picture AT82 · Jul 28, 2017

I don't see that you would need the trackBy here, unless you want to use it. But it has nothing to do with why your default option is not working.

Why it works with level.id is because this is a string (number ?) whereas level is an object that has no reference to your array, therefore it cannot be recognized from the list.

Since you are using Angular 4, we now have a new directive [compareWith] where we can compare some property from your level, for example the id. Compare it with the array and find a match. So what you can do is the following:

<select formControlName="levelControl" [compareWith]="compare" 
  id="levelSelect" name="levelSelect" class="size-xl">
    <option value="">Select</option>
    <option *ngFor="let level of referentiel.levels" [ngValue]="level">
      {{level.label }}
    </option>
</select>

component:

compare(val1, val2) {
  return val1.id === val2.id;
}

Also notice that I changed

<option [ngValue]="null">Select</option>

to

<option value="">Select</option>

so that Angular is not trying to compare to a null value. That would throw an error.