cole
cole

Reputation: 1757

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

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:

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:

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!

Upvotes: 24

Views: 15727

Answers (5)

Diogo Sousa
Diogo Sousa

Reputation: 83

The [compareWith] solves this problem. Just change in the method compareObj the correct atribute for the Object your comparing.

<select [compareWith]="compareObj"  [(ngModel)]="selectedObjects">
   <option *ngFor="let object of objects" [ngValue]="object">
      {{object.name}}
   </option>
</select>

compareObj(o1: Object, o2: Object) {
   return o1.id === o2.id;
}

Upvotes: 1

cole
cole

Reputation: 1757

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;
}

Upvotes: 22

Klinki
Klinki

Reputation: 1439

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

Upvotes: 5

Ashwin Kumar
Ashwin Kumar

Reputation: 741

There is no straight-forward approach to using ngModel binding this way. However, since you do not want to register event handlers separately, we may need to change the way the object is being assigned. Also you need not assign id for select tag as it is not serving the purpose. For the use case mentioned, I have modified it to see the two-way binding.

Find the component's constructor with model and printVal() is included just to test the change. Template does not include ngValue and ngModel, still it can work as expected.

constructor() {
    this.activePerson = {id: 'a1',
        hobbyList : [
           {
             id: 'h4'
           },
           {
             id : 'h2'
           }
          ]
      }
      
    this.hobbyListSelect = [
        {
         id: 'h1'
        },
        {
         id : 'h2'
        },
        {
         id: 'h3'
        },
        {
         id : 'h4'
        }
      ];
  }

printVal($event,i) {
    console.log(JSON.stringify(this.activePerson.hobbyList))
}
<div *ngFor='let a of activePerson.hobbyList ; let i=index; trackBy:a?.id'>
        <label for='personHobbies'>Hobby:</label>
        <select name='personHobbies' 
        (change)=" activePerson.hobbyList[i] = hobbyListSelect[$event.target.selectedIndex];
        printVal($event,i)">
            <option *ngFor='let h of hobbyListSelect; trackBy:h?.id' 
                [selected]='(h.id == a.id) ? "selected" : null'
            >
            {{h.id}}
            </option>
        </select>
    </div>

Upvotes: 0

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657058

When you get an object passed from somewhere that you want to make the selected one, just search the hobbyListSelect array for the matching item and assign the found item to activePerson.hobbyList[i]

Upvotes: 0

Related Questions