15

I am trying to populate a drop-down select options list and set a default selected value using ng-model and ng-options.

I have the following code in my view:

<select ng-model="thisTour.site" ng-options="site.name for site in siteList"></select>

And in my controller:

    $scope.siteList = [
        { id: 1, name: 'cycling'},
        { id: 2, name: 'walking'},
        { id: 3, name: 'holidays'}
    ]

    $scope.thisTour.site = { id: 2, name: 'walking'};

The list is getting populated with the correct 3 options from the siteList object, but it is not selecting walking by default as I would expect? Why not?

Now, when I change this:

$scope.thisTour.site = { id: 2, name: 'walking'};

To this:

$scope.thisTour.site = $scope.siteList[1];

Now it works. Why? Isn't it the same thing?

PSL
  • 123,204
  • 21
  • 253
  • 243
Inigo
  • 8,110
  • 18
  • 62
  • 110

4 Answers4

46

That is because angular looks for object equality to bind it with your syntax and inyour case $scope.siteList[1] is not equal to { id: 2, name: 'walking'}; (2 objects are equal only if they point to the same reference). You can get around this in many ways, one easy way is to use track by syntax with ng-options to specify track by id, which will enable ng-option's options to be tracked by the specified property of the bound object rather than the object reference itself.

<select ng-model="thisTour.site" 
    ng-options="site.name for site in siteList track by site.id"></select>

You could also use the syntax to minimally set the ng-model to specify only the id using select as part in the syntax:-

Example:-

ng-options="site.id as site.name for site in siteList"

and model would just be:-

 $scope.thisTour.site = 2;

angular.module('app', []).controller('ctrl', function($scope){
  $scope.thisTour = {};
 $scope.siteList = [
        { id: 1, name: 'cycling'},
        { id: 2, name: 'walking'},
        { id: 3, name: 'holidays'}
    ]

    $scope.thisTour.site = { id: 2, name: 'walking'};
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
  <select ng-model="thisTour.site" ng-options="site.name for site in siteList track by site.id"></select>
  {{thisTour.site}}
  </div>

From documentation

trackexpr: - Used when working with an array of objects. The result of this expression will be used to identify the objects in the array. The trackexpr will most likely refer to the value variable (e.g. value.propertyName). With this the selection is preserved even when the options are recreated (e.g. reloaded from the server).

Also worth noting:

Do not use select as and track by in the same expression. They are not designed to work together.

PSL
  • 123,204
  • 21
  • 253
  • 243
  • OK, I think I get it now, thanks. I'd read about 'track by' in passing but didn't fully understand. This is very comprehensive, thank you. – Inigo Dec 16 '14 at 21:46
  • @Inigo Yeah the documentation link is down. Take a look at the usage section in official ng-options documentation. – PSL Dec 16 '14 at 21:50
  • 2
    +1 for `Do not use select as and track by in the same expression. They are not designed to work together` – kmhigashioka Jul 13 '17 at 02:55
  • thanks kazupooot, your comment is very useful to me. – nativegrip Jul 13 '17 at 10:48
3

This isn't the same thing because objects in javascript are passed by reference.

If you take the first example:

$scope.siteList = [
    { id: 1, name: 'cycling'},
    { id: 2, name: 'walking'},
    { id: 3, name: 'holidays'}
]

$scope.thisTour.site = { id: 2, name: 'walking'};

Then you do this:

$scope.thisTour.site.id = 3;
console.log($scope.siteList[1].id) // 2

In other words, whilst your two objects are equal in value, they aren't the same object. The ngOptions directive sees this, so would set thisTour.site to a blank value because it isn't one of the allowed options.

Google "passing by reference in javascript" to learn more.

Ed_
  • 18,798
  • 8
  • 45
  • 71
  • I guess it should be copy of reference, i dont think there is any pass by reference in javascript. Incase of objects object's references are copied... It is completely different from by ref implementation in many languages. – PSL Dec 16 '14 at 21:42
  • Not sure I understand your comment. It's beyond my programming knowledge. All the javascript books I have read say javascript passes objects by reference. – Ed_ Dec 16 '14 at 21:43
  • `function updateObj(someObj){someObj = null;}` and `var ob = {test:'1'}; updateObj(ob);` This will not nullify the object someObj because what is copied to the function is the value of object's reference. Whereas say in c# which supports pass by reference with ref keyword , the originial object inturn will be nullified. – PSL Dec 16 '14 at 21:45
  • 1
    i was really struggling with binding dropdown selected value but your solution is really damn simple and helped me.Thank you sir for posting such a simple solution – I Love Stackoverflow Mar 23 '16 at 07:19
1

Since you are using the entire object in your select then when Angular does it's comparison it is going to see if the objects are the same in order to set your select. I believe there is a way to change the functionality in how Angular does it's comparisions but I just loop through the select and do my own comparions similar to the below:

$scope.siteList = [
        { id: 1, name: 'cycling'},
        { id: 2, name: 'walking'},
        { id: 3, name: 'holidays'}
    ]
angular.forEach($scope.siteList, function(site, index) {
    if (site.id == 2) {
        $scope.thisTour.site = site;
    }
});

This will set the actual object to your variable allowing it to be set in the select.

Matthew Green
  • 10,161
  • 4
  • 36
  • 54
0

Use to ng-init directive .Which is execute initialy at a time we can assign value to ng-model .

<div ng-init="thisTour.site = siteList[position]">
    <select ng-model="thisTour.site" ng-options="site.name for site in  siteList track by site.id"></select>
</div>