4

I currently have the following directive on my select.

ng-options="option.value as option.title for option in exportTypes"

$scope.exportTypes = an array of objects each with title, value, and generatesFile attributes. I'd like the generatesFile attribute to be added as a data-generates-file attribute on each <option> that is generated for this select.

Any thoughts on how to do this?

Ilan Biala
  • 3,319
  • 5
  • 36
  • 45
  • You could not use ng-options and create an option and use ng-repeat on that option then you can set attributes accordingly. – Delta Jul 14 '14 at 20:40
  • `ng-repeat` is slower and this has to be as performant as possible. [Look at the first answer for an explanation.](http://stackoverflow.com/questions/17482817/what-are-the-differences-between-ng-repeat-and-ng-options-and-why-do-they-not-be). – Ilan Biala Jul 14 '14 at 20:41
  • Without creating the option elements yourself I don't believe you're going to be able to add custom attributes to them. Angular manages selects behind the scenes for you, allowing you to get back whole objects when options are selected rather than just the value of the `value` attribute, etc. I think your best bet is going to be `ng-repeat` on the options elements. Also, how is that less performant? It's performing similar iteration logic behind the scenes when you do ng-options anyway. – CatDadCode Jul 14 '14 at 20:49
  • `ng-repeat` uses a new scope, whereas `ng-options` uses the current scope. I'm hoping I can use the former, but if not, I think I'll have to go with something like what you showed in your answer. – Ilan Biala Jul 14 '14 at 20:58
  • Gotcha. I still wonder if the creation of a new scope is really going to be an order of magnitude difference in performance, but I haven't done any benchmarking so I don't actually know. – CatDadCode Jul 14 '14 at 21:00
  • It gets more significant as the list increases, and I need to make sure that this is fast even with a bunch of options being populated. – Ilan Biala Jul 14 '14 at 21:05
  • I'm not sure you're interpreting these benchmarks correctly, and if you have a enough items to cause a performance problem you would really just have a UI problem, and you should probably use an autocomplete and not a dropdown. – Dylan Jul 14 '14 at 21:15
  • Yeah, I agree with @Dylan. I've been playing with [a plunkr](http://plnkr.co/edit/yxreWTQ8AZ24KeEiideb?p=preview) duplicating your scenario and even after adding thousands of items to the selects (in both setups) I can't tell a difference. Still figuring out how to get real benchmark numbers out of it, but as far as usability is concerned I see no difference. Each trip through the digest cycle seems to be about the same whether I'm using `ng-options` or `ng-repeat`. – CatDadCode Jul 14 '14 at 21:27
  • 1
    The other, more performant option is to use the select as intended without attributes and just use the index to locate the 'generatesFile' in the object, on change. – Dylan Jul 14 '14 at 21:41
  • @Dylan's suggestion is probably the best option. I've augmented my answer to show just how easy this is. You don't even need to use the index to reference the original object. – CatDadCode Jul 14 '14 at 21:48

3 Answers3

5

Maybe someone will correct me but I'm pretty sure you won't get this kind of control out of ng-options. Angular manages the select behind the scenes for you when you use this, which is nice 90% of the time but can be limiting in situations like yours. I'm not sure what benchmarks you're looking at but I'd be willing to bet an ng-repeat on the option elements is comparable in performance to using ng-options.

I'd go with something like this:

<select ng-model="selectedExportType">
    <option ng-repeat="exportType in exportTypes" data-generates-file="{{exportType.generatesFile}}" value="{{exportType.value}}">
        {{exportType.title}}
    </option>
</select>

Edit:

Of course, this assumes you really do need that attribute on there. If all you need is access to that attribute on select then this is where ng-options shines. Simply remove the option.value as bit from your select and then you get the whole object back from your array when you make a selection.

http://plnkr.co/edit/6XPaficTbbzozSQqo6uE?p=preview

You can see in that demo that the selected item is the whole object, complete with someAttr property which was never used in the select at all. If you inspect the DOM you won't see it. Angular tracks all this behind the scenes.

Community
  • 1
  • 1
CatDadCode
  • 58,507
  • 61
  • 212
  • 318
  • 1
    I'll try out the edited suggestion, and I'll see if that works. It seems that it would just from looking at it now though. – Ilan Biala Jul 15 '14 at 01:43
  • Well considering that I included a working demo, I think it works ;) – CatDadCode Jul 15 '14 at 02:23
  • Can't get rid of the blank first option even though I'm using `ng-init`. Thoughts? Otherwise this would be very awesome! – Ilan Biala Jul 15 '14 at 15:25
  • The blank option is probably stemming from an unassigned ng-model. If you set the ng-model to a default value in the controller the blank option will be replaced. See http://stackoverflow.com/questions/12654631/why-does-angularjs-include-an-empty-option-in-select – Jackson Hyde Dec 23 '14 at 11:36
5

Here is a directive which can be used to add custom attributes when using ng-options with <select>, so you can prevent using ng-repeat

.directive('optionsCustomAttr', function ($parse) {
        return {
            priority: 0,
            require: 'ngModel',
            link: function (scope, iElement, iAttrs) {
                scope.addCustomAttr = function (attr, element, data, fnDisableIfTrue) {
                    $("option", element).each(function (i, e) {
                        var locals = {};
                        locals[attr] = data[i];
                        $(e).attr(iAttrs.customAttrName ? iAttrs.customAttrName : 'custom-attr', fnDisableIfTrue(scope, locals));
                    });
                };
                var expElements = iAttrs['optionsCustomAttr'].match(/(.+)\s+for\s+(.+)\s+in\s+(.+)/);
                var attrToWatch = expElements[3];
                var fnDisableIfTrue = $parse(expElements[1]);
                scope.$watch(attrToWatch, function (newValue) {
                    if (newValue)
                        scope.addCustomAttr(expElements[2], iElement, newValue, fnDisableIfTrue);
                });
            }
        };
     })

And then in your select,

<select ng-model="selectedExportType" ng-options="option.value as option.title for option in exportTypes" 
                  options-custom-attr="option.generatesFile for option in exportTypes" 
                  custom-attr-name="data-generates-file">
</select>

Note: This is a modified version of optionsDisabled directory mentioned here

Community
  • 1
  • 1
vtor
  • 8,989
  • 7
  • 51
  • 67
  • You can replace the jquery based for each loop with native `angular.foreach`. Adding the 'title' property can also be done using `elem.attr` – sugavaneshb Aug 02 '16 at 15:36
  • I had to make two changes to make this work properly: Using `data[i - 1]` caused it to leave the first option without the attribute and all the following options with the wrong value. `data[i]` worked for me. Since the directive makes reference to `iAttrs.customAttrName`, I had to put `custom-attr-name` in the HTML instead of `custom-attr` – MichaelPlante Aug 03 '16 at 15:04
  • Taking into account @MichaelPlante's comment you didn't even test if the code you posted works... – Piotr Dobrogost Nov 29 '16 at 15:04
-1

Or a simpler solution:

<select ng-model="selectedItem" ng-options="item as item.name for item in items" option-style="items"></select>

And the directive which inserts whatever you want in each option tag.

angular.directive('optionStyle', function ($compile, $parse) {
    return {
        restrict: 'A',
        priority: 10000,
        link: function optionStylePostLink(scope, elem, attrs) {
            var allItems = scope[attrs.optionStyle];
            var options = elem.find("option");
            for (var i = 0; i < options.length; i++) {
                angular.element(options[i]).css("color", allItems[i].yourWantedValue);
            }
        }
    };
});
trd
  • 95
  • 1
  • 8