Using ngOptions In AngularJS

Wednesday, June 19, 2013

The ngOptions directive in AngularJS allows you to build and bind an HTML select element with options to a model property. It’s quite a versatile directive and is smarter than trying to build the select options with ngRepeat since ngOptions is optimized for two way data binding.

There are some tricks, however.

Let’s start with the following simple controller in JavaScript.

var EngineeringController = function($scope) {

    $scope.engineer = {
        name: "Dani",
        currentActivity: "Fixing bugs"
    };

    $scope.activities =
    [
        "Writing code",
        "Testing code",
        "Fixing bugs",
        "Dancing"
    ];        
};

The goal is to build a drop down list to select an engineer’s current activity.

<div data-ng-controller="EngineeringController">
    {{engineer.name}} is currently: {{ engineer.currentActivity}}
    <div>
        Choose a new activity:
        <select data-ng-model="engineer.currentActivity" 
                data-ng-options="act for act in activities">                
        </select>
    </div>
</div>

One of the tricks to using ngOptions is figuring out the expression AngularJS expects. The current value of “act for act in activities” is telling AngularJS to use the value of each entry in the activities array. This syntax against a simple array of strings allows the select element to appear with the engineer’s current activity selected, and if the selected option changes the framework updates the engineer’s current activity (and vice versa).

image

The expression you can use with ngOptions can be quite a bit more advanced (which could be good or bad). For example, instead of using strings let’s use objects to represent an activity.

var EngineeringController = function($scope) {

    $scope.engineer = {
        name: "Dani",
        currentActivity: {
            id: 3,
            type: "Work",
            name: "Fixing bugs"
        }
    };

    $scope.activities =
    [
        { id: 1, type: "Work", name: "Writing code" },
        { id: 2, type: "Work", name: "Testing code" },
        { id: 3, type: "Work", name: "Fixing bugs" },
        { id: 4, type: "Play", name: "Dancing" }
    ];        
};

And we’ll change the ngOptions expression to build a label for the select option.

<select data-ng-model="engineer.currentActivity" 
        data-ng-options="a.name +' (' + a.type + ')' for a in activities">                
</select>

This produces:

image

Expressions can also create optgroup elements when using a group by in the expression.

<select ng-model="engineer.currentActivity" 
        data-ng-options="a.name group by a.type for a in activities">                
</select>

Which yields:

image

What About The Initial Selection?

When we switched from an array of strings to activities as objects, we lost the ability for the select to show the starting currentActivity as initially selected. If the user selects a new activity the two-way data binding works and the currentActivity is set, but we lost the initial selection. This is because AngularJS is doing reference comparisons and employee.currentActivity might by “Fixing bugs”, but employee.currentActivity != activities[2], so the page starts with an empty selection in the drop down list.

This is a common occurrence since the engineer object and the activities list are most likely de-serialized from HTTP calls and will completely different object graphs.  There is no good solution with the exiting ngOptions directive but to “fix up” the engineer once the engineer and all the possible activities are loaded. Something like the following (which could be shortened with a library like undercore.js).

for (var i = 0; i < $scope.activities.length; i++) {
    if ($scope.activities[i].id == $scope.engineer.currentActivity.id) {
        $scope.engineer.currentActivity = $scope.activities[i];
        break;
    }
}

One Last Variation

What if you had objects representing activities but only wanted the Id property of the selected activity instead of the entire activity. The ngOptions expression can handle this scenario, too. Inside the controller would look like this:

$scope.engineer = {
    name: "Dani",
    currentActivityId: 3
};

$scope.activities =
[
    { id: 1, type: "Work", name: "Writing code" },
    { id: 2, type: "Work", name: "Testing code" },
    { id: 3, type: "Work", name: "Fixing bugs" },
    { id: 4, type: "Play", name: "Dancing" }
];

And the expression would use a select as value syntax.

<select ng-model="engineer.currentActivityId" 
        data-ng-options="a.id as a.name group by a.type for a in activities">                
</select>

And that concludes this post on ngOptions. I hope you found the topic stimulating, and stayed on the edge of your seat until these final words.


Comments
gravatar Ali Hmer Wednesday, June 19, 2013
As ever, great post.
gravatar Chris Gruel Wednesday, June 19, 2013
Very nice article, thanks for sharing!
gravatar Ken Wednesday, June 19, 2013
I use underscore js to set the default. select ng-model="sel" ng-init="def_id=1" In controller. $scope.sel= _.find($scope.collection, function (el) { return el.id = $scope.id; });
gravatar Wolf Wednesday, June 19, 2013
Good stuff. This should be added to the official docs.
gravatar John Wednesday, June 19, 2013
Nice! Always wondered about those expressions, and this post does a great job explaining them
gravatar Louis Haußknecht Thursday, June 20, 2013
There's a new feature for ngRepeat where you can say "track by a.Id". https://github.com/angular/angular.js/commit/61f2767ce65562257599649d9eaf9da08f321655 Maybe that should be ported to ngOptions as well to support custom tracking of objects.
gravatar sai Thursday, June 20, 2013
how about Multi select options. which make it complete....
gravatar James Bell Friday, June 21, 2013
Nice article, I just added one of these to my first Angular project a few days back. A common next task for anyone creating these is triggering an action based on a change to the select list. I used ngChange but read about other options. That article would make a nice followup.
gravatar Chad Smith Saturday, June 22, 2013
We encountered the problem of preselecting an option so frequently and it was causing so much boilerplate code that we came up with a solution: http://configit.github.io/ngyn/#select_extensions We found if we used the "x.id in for x in choices" that we usually wanted more than just the id back out and ended up doing a lookup into the array anyway, so that didn't work out any better. Supplying a key as in our solution has worked out perfectly. I was expecting when they announced the addition to ngRepeat for them to announce the same for select but it never came.
gravatar Igor Monday, July 8, 2013
I'd like to write something more about keyword "track by" (already mentioned by Louis). Let's suppose you have model ( called dmodel ) containing an array of values [ 3,4,5,6,7 ]. You define then [select]: [select ng-model="dmodel" ng-options="v as v in dmodel" /]. You play then with values from javascript code - looks perfect! Then you look at the html and what is underneath? You see [option value="0"]3[/option][option value="1"]4[/option] and so on. Guess what is sent to the server? Yep. You're right. ( yes, i know, i should not use old-fashioned-way data transfer, but i wanted ). How to force angular to put real values instead of index values ? Use track by keyword. Syntax is following: ng-options="name in collection track by value" . But be careful: you cannot write ng-options="value as name in collection track by value", cos it will correctly html, but you will not be able to pick the selection :-) Hope this will save you few days of your work.
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!