Setting the selected item in angularJS select directive

61,701

Solution 1

It is as simple as this

<select
    ng-model="item"
    ng-options="item.name for item in items track by item.name">

Then inside you controller:

// all items
$scope.items = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
// set the active one
$scope.item = {name: 'b'};
// or just
$scope.item = $scope.items[1]

Check out the http://docs.angularjs.org/api/ng.directive:select From there:

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).

The rest is just assigning a value to the $scope.item variable and angular will figure out which element should be set as active by checking the item's name property.

Solution 2

The reason it doesn't work is that angular expects the objects references to be equal. In your case (the 'select from object' in your plnkr) creates a new object, albeit with the same properties. However, Angular can't know that two different objects represents the same object. You have at least two approaches:

Find the correct city object instance

Instead of setting $scope.customer.city to a new object, find the actual city object from the $scope.cities array. If you're using UnderscoreJs you could do something like:

$scope.customer.city = _.find($scope.cities, function (city) {
    return city.id === theCustomersCity.id;
});

Bind to the city id instead of the city object

Another approach, which might be easier, is to change the ng-model and ng-options directives to match on id instead of object. See working example here.

<select ng-model="customer.cityId" ng-options="i.id as i.name for i in cities"></select>

Solution 3

I came accross the same problem. My options and the modeled data both came from separate API calls.

Instead of adding a layer of indirection by using ng-model on the object keys, I ended up writing a simple directive that uses a "proxy" variable.

<select ng-model="customer.city" ng-options="i as i.name for i in cities"></select>

becomes

<select ng-model="customer_cityProxy" ng-options="i.name as i.name for i in cities"></select>

Using a $watch on customer.city and customer_cityProxy, I get the expected behaviour.

There are still a few drawbacks, on being that it only works if the keys are disjoints.

Code is available here: https://github.com/gosusnp/options-proxy

Share:
61,701

Related videos on Youtube

javito
Author by

javito

Updated on January 26, 2020

Comments

  • javito
    javito over 4 years

    I have a problem with setting the selected item in angular's select directive. I don't know if this is a bug or a conscious design from the designers of angular. It sure makes the select directive a lot less useful though.

    Description:

    My app communicates with a REST API to receive an entity from the database. The API dictates that relations of the object are sent with an ID property only so that you can retrieve them in subsequent requests if needed.

    Example:

    { id : 1, customerName : "some name", city : {id : 12}} 
    

    where city is another entity that can be retrieved through a different REST endpoint using the city id and looks like so:

    { id: 12, name : "New York"}
    

    I need to create a form to edit the customer entity with a dropdown menu with all possible cities so that the user can select the appopriate city from the list. The list must initially display the customer's city as retrieved from the JSON object.

    The form looks like this:

     <form>
      <input type="text" ng-model="customer.name"/>
      <select ng-model="customer.city" ng-options="i as i.name for i in cities"></select>
     </form> 
    

    And the controller looks like this:

    app.controller('MainCtrl', function ($scope, $http) {
        $http.get(serviceurl + 'admin/rest/customer/' + id + "/", {
            params: {"accept": "json"},
            withCredentials: true
        }).then(function (response) {
                    $scope.customer = response.data.item;
                });
        $http.get(serviceurl + 'admin/rest/city/', {
            params: {"accept": "json"},
            withCredentials: true
        }).then(function (response) {
                    $scope.cities = response.data.items;
                    // THIS LINE LOADS THE ACTUAL DATA FROM JSON
                    $scope.customer.city = $scope.findCity($scope.customer.city);
                });
        $scope.findCity = function (city) {
            for (var i = 0; i < $scope.cities.length; i++) {
                if ($scope.cities[i].id == city.id) {
                    return $scope.cities[i];
                }
            }
        }
    });
    

    What should happen: once the full details of the City object are loaded the select directive must set the city that was loaded as the selected item in the list.

    What happens: the list displays an empty item and there's no way to initialize the selected item if the selected item from objects outside the array of items.

    DEMO of the issue here: http://plnkr.co/edit/NavukDb34mjjnQOP4HE9?p=preview

    Is there a solutions for this? Can the selected item be set programmatically in a generic way so that the AJAX calls and select logic be refactored into a reusable AJAX based select widget ?

  • javito
    javito over 11 years
    As a matter of fact I do find the correct city: $scope.customer.city = $scope.findCity($scope.customer.city); Any ideas why your code would work, where mine fails?
  • Martin
    Martin over 11 years
    Can it be a race condition that your second ajax call (admin/rest/city/) is completed before the first ('admin/rest/customer/' + id)?
  • Pakman
    Pakman about 10 years
    This appears to be the best answer - trackexpr is where it's at
  • hutingung
    hutingung almost 10 years
    Why not accepted as answer? I think it will save a lot of people time.
  • Christophe Fondacci
    Christophe Fondacci almost 10 years
    +1 for most elegant answer! Did not know about the 'track by' expression. This answer should be accepted.
  • Eric
    Eric over 9 years
    I agree with others that this is the best answer. I was puzzled why what seemed like an identical object on my controller scope was not being selected. 'track by' was the key. Thanks!
  • simo
    simo about 9 years
    @wusauarus you missed to add track by i.name at the end
  • wusauarus
    wusauarus about 9 years
    Got it! Thank you @simo
  • Mimu
    Mimu about 9 years
    Thanks dude, saved me a lot of time.
  • Juan
    Juan about 8 years
    I used the approach with the id and my problem was related to send a string instead of an integer