41

I have an array of Person objects

var persons = [
{Name:'John',Eligible:true},
{Name:'Mark',Eligible:true},
{Name:'Sam',Eligible:false},
{Name:'Edward',Eligible:false},
{Name:'Michael',Eligible:true}
];

and i am using select with ng-options like this:

<select ng-model="Blah" ng-options="person.Name for person in persons"></select>

I want to show the record with Eligible:false in red color. So the problem is how do i use the ng-class in select inorder to achieve this? Since we are not using any option tag it wont work if i simply add ng-class in the select element itself.

Dor Cohen
  • 16,769
  • 23
  • 93
  • 161
I_Debug_Everything
  • 3,776
  • 4
  • 36
  • 48
  • use a directive to loop over options and apply class to option tags that meet condition – charlietfl Mar 07 '13 at 06:46
  • yeah i can do that but isn't there any way to do it directly? I mean there must be a way to use the ng-class here. – I_Debug_Everything Mar 07 '13 at 08:12
  • FYI: I've updated my answer because there were a few bugs in it. Not sure if this effects anything in your current codebase, but if you are using what I posted before you may want to have a look. – Ben Lesh Nov 30 '13 at 03:18

9 Answers9

36

You could create a directive that processed the options after the ngOptions directive is processed that updated them with the appropriate classes.

Update: The old code had a few bugs, and I've learned a bit since I answered this question. Here is a Plunk that was redone in 1.2.2 (but should work in 1.0.X as well)

Here is updated (Nov 30 '13 at 3:17) the Code:

app.directive('optionsClass', function ($parse) {
  return {
    require: 'select',
    link: function(scope, elem, attrs, ngSelect) {
      // get the source for the items array that populates the select.
      var optionsSourceStr = attrs.ngOptions.split(' ').pop(),
      // use $parse to get a function from the options-class attribute
      // that you can use to evaluate later.
          getOptionsClass = $parse(attrs.optionsClass);

      scope.$watch(optionsSourceStr, function(items) {
        // when the options source changes loop through its items.
        angular.forEach(items, function(item, index) {
          // evaluate against the item to get a mapping object for
          // for your classes.
          var classes = getOptionsClass(item),
          // also get the option you're going to need. This can be found
          // by looking for the option with the appropriate index in the
          // value attribute.
              option = elem.find('option[value=' + index + ']');

          // now loop through the key/value pairs in the mapping object
          // and apply the classes that evaluated to be truthy.
          angular.forEach(classes, function(add, className) {
            if(add) {
              angular.element(option).addClass(className);
            }
          });
        });
      });
    }
  };
});

Here's how you'd use it in your markup:

<select ng-model="foo" ng-options="x.name for x in items" 
        options-class="{ 'is-eligible' : eligible, 'not-eligible': !eligible }">
</select>

It works like ng-class does, with the exception that it's on a per-item-in-the-collection basis.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • 1
    That's a great solution. There is always directive to save the day!! :D +1 – I_Debug_Everything Mar 08 '13 at 03:38
  • 1
    It was a fun one to figure out. I liked to part where I created a new scope and extended it with the child I was examining so I could $eval it! So Angular, so very Angular. Makes me want to make laser noises while I code... pew pew pew. – Ben Lesh Mar 08 '13 at 18:08
  • blesh, your directive does NOT work correctly. The classes applied before selection of any – Kulbi Nov 29 '13 at 12:55
  • 1
    @CloudRide, stop yelling. :P ... what version of Angular? – Ben Lesh Nov 30 '13 at 02:03
  • @CloudRide, thank you for drawing this to my attention. I'm updating the answer with something that works more consistently. Also it's done in 1.2.2 (but it should work in any version north of 1.0.7, I think) – Ben Lesh Nov 30 '13 at 03:12
  • 1
    @FlorenceFoo definitely, but you'll need to select the option tags and loop through them checking the value on each like so: http://jsbin.com/jejim/1/edit?html,css,js,output – Ben Lesh Jul 28 '14 at 16:28
  • What is ngSelect in the link parameter list for? I couldnt find any info in the documentation – Jay Jay Jay Mar 24 '15 at 17:54
  • In my case, the list of options is mutated after the link function is called because I am fetching the array of options via `$resource`. To ensure that the `$watch` handler is always called once the array of options is populated, I had to add `true` as the third argument to `scope.watch()` in your directive. – Jannik Jochem May 12 '15 at 16:04
  • Actually, this was not quite enough to get the directive to behave correctly. Here is a plunker that works: http://plnkr.co/edit/duiAehJNdoBsYqnst5ad. I had to use `scope.$watchCollection` as well as `scope.$$postDigest` in order to ensure that the classes are added after `ng-options` has already updated the `options` elements. – Jannik Jochem May 12 '15 at 16:36
  • Nice fix! I had to add the following to get it to strip out filtering I had in place inside my ng-options attribute: `// get the source for the items array that populates the select.` `var optionsFiltersStripped = (attrs.ngOptions.indexOf('|') > -1) ? attrs.ngOptions.split('|')[0] : attrs.ngOptions;` `var optionsSourceStr = optionsFiltersStripped.trim().split(' ').pop(),` `// use $parse to get a function from the options-class attribute` `// that you can use to evaluate later.` `getOptionsClass = $parse(attrs.optionsClass);` – Ben Barreth Jul 10 '15 at 15:46
  • 4
    This code breaks with angularjs 1.4 and higher, but works fine in previous versions. – Jared Aug 19 '15 at 19:48
  • This does not work in 1.4.7 version. Any suggestion what needs to be changed to make it work in 1.4.7? I've tried the plunker and changed to 1.4.7 and it's confirmed, it does not work. – Strategist Oct 23 '15 at 10:33
  • 1
    The value given by default to an option changed in angular 1.4.x. Before 1.4.x this value was based on the index. See the code above how the index taken in the forEach is used to set the value to the options. After 1.4.x the value by default is the item.$hashKey something like object:1234. Change the line elem.find('option[value=' + index + ']'); to elem.find('option[value="' + item.$hashKey + '"]'); and it will work option. But consider to use track by to give the the option some known value – yeraycaballero Mar 14 '16 at 11:48
  • None of this works anymore in 1.5.8, not even the adjustment above by @yeraycaballero. – Grandizer Oct 13 '16 at 11:57
  • 1
    @BenLesh Your solution still works on Angular 1.6 with a few modifications: http://plnkr.co/edit/NpJUjYOUIYs5Yso7A1eo?p=preview Angular now links to the items by their `$$hashkey` rather than index when not using `track by`. Otherwise it'll use whatever track by property you're using. My solution is dirty, but works for anyone needing a starting point! – Coo Jan 04 '17 at 06:40
15

In this scenario you can only apply ng-class only if you use ng-repeat with option tags:

<select ng-model="Blah">
  <option ng-repeat="person in persons" ng-class="{red: person.Eligible}">
    {{person.Name}}
  </option>  
</select>

This will give custom class to your 'Eligible' persons, but CSS won't work consistently across bowsers.

Plunker.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Stewie
  • 60,366
  • 20
  • 146
  • 113
  • 1
    this works but i want to use with ng-options since this wont let any value selected once i save it in my database and refresh it. do you have any idea how to keep my record selected? i have already used ng-selected for this but that too wont work – I_Debug_Everything Mar 07 '13 at 10:30
  • That's not a problem. I attached the Plunker link to the answer. – Stewie Mar 07 '13 at 10:38
  • Thanks man!! i was using the same thing. Only the difference being i was using jquery version -1.7 and thus it wasn't supporting ng-selected. Changed the version reference and it works wonder!! – I_Debug_Everything Mar 07 '13 at 12:18
  • sorry to unmark your answer as correct answer. Your solution works fine but it still doesn't solve the basic need of my question i.e using ng-class with select having ng-options. Please don't mind – I_Debug_Everything Mar 07 '13 at 12:52
  • 1
    That's not a problem, but in any case you won't be able to instruct Angular to use ng-class with ng-options in order to apply custom classes to generated option tags. – Stewie Mar 07 '13 at 14:06
  • Can use with "multiple" attribute [as seen here](http://plnkr.co/edit/GtoIYH4MX8XwZh4Nnqxv?p=preview) – Nate Anderson Mar 09 '16 at 21:55
4

I wanted to comment on the accepted answer, but because I don't have enough reputation points, I must add an answer. I know that this is an old question, but comments where recently added to the accepted answer.

For angularjs 1.4.x the proposed directive must be adapted to get it working again. Because of the breaking change in ngOptions, the value of the option isn't anymore the index, so the line

option = elem.find('option[value=' + index + ']');

won't work anymore.

If you change the code in the plunker to

<select ng-model="foo" ng-options="x.id as x.name for x in items" 
        options-class="{ 'is-eligible' : eligible, 'not-eligible': !eligible }">
</select>

As result the value of the option tag will now be

value="number:x" (x is the id of the item object)

Change the directive to

option = elem.find('option[value=\'number:' + item.id + '\']');

to get it working again.

Of course this isn't a generic solution, because what if you have not an id in your object? Then you will find value="object:y" in your option tag where y is a number generated by angularjs, but with this y you can't map to your items.

Hopes this helps some people to get their code again working after the update of angularjs to 1.4.x

I tried also to use the track by in ng-options, but didn't get it to work. Maybe people with more experience in angularjs then me (= my first project in angularjs)?

georgeawg
  • 48,608
  • 13
  • 72
  • 95
evb
  • 75
  • 1
  • 6
3

The directive is one way, but I used a custom filter. If you know how to select your element, you should be fine here. The challenge was to find the current option element inside the select. I could have used the "contains" selector but the text in the options may not be unique for items. To find the option by value, I injected the scope and the item itself.

<select ng-model="foo" ng-options="item.name|addClass:{eligible:item.eligible,className:'eligible',scope:this,item:item} for item in items"></select>

and in the js:

var app = angular.module('test', []);

app.filter('addClass', function() {
  return function(text, opt) {
    var i;
    $.each(opt.scope.items,function(index,item) {
      if (item.id === opt.item.id) {
        i = index;
        return false;
      }
    });
    var elem = angular.element("select > option[value='" + i + "']");
    var classTail = opt.className;
    if (opt.eligible) {
      elem.addClass('is-' + classTail);
      elem.removeClass('not-' + classTail);
    } else {
      elem.addClass('not-' + classTail);
      elem.removeClass('is-' + classTail);
    }
    return text;
  }
})

app.controller('MainCtrl', function($scope) {
  $scope.items = [
    { name: 'foo',id: 'x1',eligible: true},
    { name: 'bar',id: 'x2',eligible: false}, 
    { name: 'test',id: 'x3',eligible: true}
  ];
 });

Here you can see it work.

westor
  • 1,426
  • 1
  • 18
  • 35
  • 1
    Actually just do away with sending the items collection into the filter (via your scope object). Use an index like this – Craig Nov 25 '14 at 03:04
  • Yes, you are right, thank you for your advice. As I made that suggestion I was not aware, how expensive filters in general are and that injecting scope via parameter is not part of best practices. – westor Nov 25 '14 at 16:36
  • I wouldn't use a filter for this. Generally filters need to be usable in services and controllers as well. Doing DOM manipulations breaks this. Directive's are designed for that. – Dormouse Dec 15 '14 at 11:15
3

The accepted answer did not work for me, so I found an alternative without a custom directive using track by :

<select ng-model="foo" ng-options="x.name for x in items track by x.eligible"></select>

Each option now gets the value x.eligible. In CSS you can style options with value = true (I think true has to be a string). CSS:

option[value="true"]{
    color: red;
}
chriscross
  • 413
  • 5
  • 18
3

In case you not only want to show them in red color but prevent the user from selecting the options, you can use disable when:

<select 
    ng-model="Blah"
    ng-options="person.Name disable when !person.Eligible for person in persons">
</select>

You can then use CSS to set the color of disabled options.

lex82
  • 11,173
  • 2
  • 44
  • 69
2

I can't write this as a comment, due to reputation, but I have updated the plunker for the accepted answer to work with Angular 1.4.8. Thanks to Ben Lesh for the original answer, it helped me a lot. The difference seems to be that newer Angular generates options like this:

<option class="is-eligible" label="foo" value="object:1">foo</option>

so the code

option = elem.find('option[value=' + index + ']');

wouldn't be able to find the option. My change parses ngOptions and determines what field of item was used for the label, and finds the option based on that instead of value. See:

http://plnkr.co/edit/MMZfuNZyouaNGulfJn41

2

I know I am a bit late to the party, but for people who want to solve this with pure CSS, without using a directive you can make a css class like this:

select.blueSelect option[value="false"]{
    color:#01aac7;
}

This css rule says : Find all elements with value = false with tag name 'option' inside every 'select' that has a class "blueSelect" and make the text color #01aac7; (a shade of blue)

In your case your HTML will look like this:

<select class="form-control blueSelect" name="persons" id="persons1"
        ng-options="person as person.name for person in $ctrl.persons track by person.Eligible"
        ng-model="$ctrl.selectedPerson" required>
    <option disabled selected value="">Default value</option>
</select>

The track by inside the ng-options is what will hold what to track the options by, or the "value" field of each option. Notice that depending on your project needs , you might have to do some tweaking to make this work as per your requirements.

But that's not going to work right when there's multiple options with the same value for the Eligible field. So to make this work, we create a compound expression to track by, that way we can have unique values to track by in each option. In this case we combine both fields Name and Eligible

So now our html will look like this

<select class="form-control blueSelect" name="persons" id="persons2"
        ng-options="person as person.name for person in $ctrl.persons track by (person.name + person.Eligible)"
        ng-model="$ctrl.selectedPerson" required>
    <option disabled selected value="">Default value</option>
</select>

and our css :

select.blueSelect option[value*="False"]{
    color:#01aac7;
}

Notice the * next to value, this is a regular expression which means to find the word "False" somewhere in the value field of the option element.

Quick Edit You can also choose to disable the options with Eligible = False using the "disable when" in the ng-options expression , for example:

label disable when disable for value in array track by trackexpr

I'll leave how to use that in your case for you to find out ;-)

This works for simple css modifications, for more complex stuff you might need a directive or other methods. Tested in chrome.

I hope this helps someone out there. :-)

georgeawg
  • 48,608
  • 13
  • 72
  • 95
RGonzalez
  • 295
  • 3
  • 7
-1

I've found another workaround that was easier than adding a directive or filter, which is to add a handler for the onfocus event that applies the style.

angular.element('select.styled').focus( function() {
  angular.element(this).find('option').addClass('myStyle');
});
Rob
  • 781
  • 5
  • 19