angular2 ngModel/ngValue select option object - equality across different instances

cole picture cole · Oct 30, 2016 · Viewed 12.2k times · Source

Flavors of this question have been asked numerous times across the various versions of Angular2 prior to release. However, I have yet to find anything that will produce the desired behavior (short of workarounds that I want to avoid) in the plunk here: Select Object Problems

My form select has 2-way binding via [(ngModel)] to an object, and then 'option' is generated via *ngFor for a list of similar objects (all differentiated by id). In my research, it has been mentioned several times that Angular2 uses JavaScript object equivalence (by instance), so an object in the bound model that does not have the same instance will not match to the list. Thus, it does not get presented as the "selected" item - breaking the "2-way" data binding.

However, I would like to define a way for these instances to match. Some solutions that seem to be floating around on the internet have been attempted, but I am either missing a small piece or have implemented incorrectly.

Options I want to avoid:

  • Binding to something other than the object (i.e. the id) in ngModel. ngValue is a great help that I want to utilize
  • Workarounds through change handlers
  • Forcing the instances to match (I am getting objects from a data service, and do not want to redefine them all to match... that seems like needless waste of resources)

Ideally (and this seems to have been discussed as possible in several places that had solutions - solutions insufficient in my case), it would be possible to define a standard of equality for ngModel to use in place of object instance equality.

i.e. the latest attempt below, where h.id == a.id is defining the attribute "selected". What I do not understand is why this "selected" attribute does not get rendered - is it blocked somehow by ngModel? Setting selected='true' manually in the HTML seems to fix, but generating with [attr.selected] or any of the other variants that build a ng-reflect-selected='true' attribute does not seem to do the trick.

<div *ngFor='let a of activePerson.hobbyList ; let i=index; trackBy:a?.id'>

    <label for='personHobbies'>Hobby:</label>
    <select id='personHobbies' class='form-control'
        name='personHobbies' [(ngModel)]='activePerson.hobbyList[i]' #name='ngModel'>

        <option *ngFor='let h of hobbyListSelect; trackBy:h?.id' 
            [ngValue]='h' 
            [attr.selected]='h.id == a.id ? true : null'
        >
        {{h.name}}
        </option>
    </select>
</div>

Some things I have tried:

  • trackBy
  • Binding to [selected]=, selected={{}}, and [attr.selected] (This seems close)

I have successfully achieved rendered HTML that looks like this:

<select ...>
    <option selected='true'>Selected</option>
    <option selected='false'>Not Selected</option>
    <!-- and variants, excluding with selected=null-->
</select>

But still no selected value when the object instance is different. I have also struck out trying to find out what element in HTML or CSS is recording the selected value when the user selects a value (how ngModel handles, and what other options there might be for handling).

Any help would be greatly appreciated. The goal is to get the "Change" button to change the underlying model and update the select boxes accordingly - I have focused my attempts on the "hobbyList." I have tried on Firefox and Chrome. Thanks!

Answer

cole picture cole · Jun 27, 2017

Per @Klinki

Currently there is no simple solution in angular 2, but in angular 4 this is already addressed since beta 6 using compareWith - see https://github.com/angular/angular/pull/13349

To illustrate the usage for the proposed case (see plunk):

<div *ngFor='let a of activePerson.hobbyList ; let i=index;'>

        <label for='personHobbies'>Hobby:</label> 
        <select id='personHobbies' class='form-control'
          name='personHobbies' [(ngModel)]='activePerson.hobbyList[i]'
          [compareWith]='customCompareHobby'>
          <option *ngFor='let h of hobbyListSelect;' [ngValue]='h'>{{h.name}}</option>
        </select>

</div>
...
customCompareHobby(o1: Hobby, o2: Hobby) {
    return o1.id == o2.id;
}