65

I'm trying to use ng-options with a <select> to bind a numeric integer value to a list of corresponding options. In my controller, I have something like this:

myApp.controller('MyCtrl', function() {
    var self = this;

    var unitOptionsFromServer = {
        2: "mA",
        3: "A",
        4: "mV",
        5: "V",
        6: "W",
        7: "kW"
    };

    self.unitsOptions = Object.keys(unitOptionsFromServer).map(function (key) {
        return { id: key, text: unitOptionsFromServer[key] };
    });

    self.selectedUnitOrdinal = 4; // Assume this value came from the server.
});

HTML:

<div ng-controller="MyCtrl as ctrl">
    <div>selectedUnitOrdinal: {{ctrl.selectedUnitOrdinal}}</div>
    <select ng-model="ctrl.selectedUnitOrdinal" ng-options="unit.id as unit.text for unit in ctrl.unitsOptions"></select>
</div>

And here's a jsFiddle demonstrating the problem, and some other approaches I've taken but am not happy with.

The select option is being initialized with an empty value, instead of "mV" as expected in this example. The binding seems to work fine if you select a different option -- selectedUnitOrdinal updates properly.

I've noticed that if you set the initial model value to a string instead of a number, then the initial selection works (see #3 in the fiddle).

I really would like ng-options to play nice with numeric option values. How can I achieve this elegantly?

BrandonLWhite
  • 1,866
  • 1
  • 23
  • 26

12 Answers12

107

Angular's documentation for the ng-select directive explains how to solve this problem. See https://code.angularjs.org/1.4.7/docs/api/ng/directive/select (last section).

You can create a convert-to-number directive and apply it to your select tag:

JS:

module.directive('convertToNumber', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ngModel) {
      ngModel.$parsers.push(function(val) {
        return val != null ? parseInt(val, 10) : null;
      });
      ngModel.$formatters.push(function(val) {
        return val != null ? '' + val : null;
      });
    }
  };
});

HTML:

<select ng-model="model.id" convert-to-number>
  <option value="0">Zero</option>
  <option value="1">One</option>
  <option value="2">Two</option>
</select>

Note: I found the directive from the doc does not handle nulls so I had to tweak it a little.

Christophe L
  • 13,725
  • 6
  • 33
  • 33
  • 3
    most of time would be *convert-to-string*, just change *parseInt* to *String* – kenberkeley Dec 25 '15 at 07:55
  • This should be the accepted answer because it's more general and doesn't depend on `ng-options`. Also, please keep in mind that this is not for any number but only for integers. If you want floats to work, use `parseFloat()` but if you're comparing floats for equality you're probably doing something very very wrong. – f.ardelian Jan 25 '16 at 18:23
  • 2
    Nice solution. But when you have 0 in your select options,it gives null because of the implementation. slight tweak is needed to handle this case. – user1776573 Mar 22 '16 at 11:44
  • Nice! I was looking for a way to convert the string values from the form to my strong typed object (with date, numbers, boolean etc). This approach looks nice! – Maarten Kieft May 17 '16 at 19:07
  • Awesome, but this didn't work for me when the value was 0 because 0 == false. Rewrote it to: return (val||0===val) ? parseInt(val, 10) : null; and return (val||0===val) ? '' + val : null; – Terrabythia Aug 15 '16 at 15:07
  • Not working for me. Still see angularjs error "Model is not of type `number`" – mimic Apr 24 '18 at 19:11
  • I have been searching for issue for atleast 2hrs or more. you just solved my issue.Thanks – Mubashar Shahzad Oct 24 '18 at 08:26
46

Maybe it's a bit messy, but result can be achieved without special functions right in ng-options

<select ng-model="ctrl.selectedUnitOrdinal" ng-options="+(unit.id) as unit.text for unit in ctrl.unitsOptions"></select>
re-gor
  • 1,311
  • 11
  • 17
  • 7
    It's really the simplest answer. The angularJS library version 1.3.10 used in the [jsFiddle](http://jsfiddle.net/LatencyMachine/x75t70pd) had a bug and +unit.id notation didn't work, but it does from 1.4.0; see the [working jsFiddle](http://jsfiddle.net/x75t70pd/4/) – cnlevy Mar 13 '16 at 21:26
  • The accepted answer did not work in combination when using the select clause in ng-otpions. This did work however. – Oskar Sjöberg May 17 '16 at 08:23
  • 3
    but the value of option is: value="number:0",with 'number' – YETI Jul 07 '16 at 15:20
  • 1
    messy? This is gorgeous! Far cleaner and simpler than injecting a custom directive. Thank you! – Ben Barreth Apr 25 '17 at 18:13
  • AMAZING!! Why is this not documented better?? Thanks – TheKido Aug 13 '17 at 23:45
  • 1
    This is really the right way to go here and helped me out a lot. Can you explain the significance of the "+" in the ng-options? – istrupin Jun 15 '18 at 22:35
  • 1
    @istrupin this is just type conversion. More on MDN: [Unary plus operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Unary_plus) – re-gor Jun 17 '18 at 11:01
17

It's because when you get the unit.id it's returning a string not an integer. Objects in javascript store their keys as strings. So the only way to do it is your approach of surrounding 4 by quotations.

Edit

<select ng-model="ctrl.selectedUnitOrdinal" ng-options="convertToInt(unit.id) as unit.text for unit in ctrl.unitsOptions"></select>

$scope.convertToInt = function(id){
    return parseInt(id, 10);
};
Mathew Berg
  • 28,625
  • 11
  • 69
  • 90
  • 1
    Thanks. I get a numeric value from the server, and I need to send a numeric value back. There is a lot of this. I really want to avoid writing conversion code in the controller. (It seems like I never had this snag in Knockout). What's a smart/lazy way I can get what I want? – BrandonLWhite Jan 23 '15 at 17:08
  • Angular is more restrictive then knockout in this aspect i guess :). Anyways I've edited the question to show you one option you can do. – Mathew Berg Jan 23 '15 at 17:19
  • 1
    Ok, I can almost live with that. – BrandonLWhite Jan 23 '15 at 17:22
  • AHA! I've chewed on this a little more and thanks to your answer and some insight from a coworker, I can get what I want by instead doing `{ id: parseInt(key), text: self.unitOptionsFromServer[key] }` when building up the options array and everything works as expected then with ng-options. The part that was tripping me up is even though you can express object keys as numeric values, JS changes those to strings. – BrandonLWhite Jan 23 '15 at 18:52
  • Totally my bad, should of noticed that you were converting it, I just quickly looked at the dom. Glad you figured it out! Side note i'd put parseInt(key, 10); – Mathew Berg Jan 23 '15 at 19:42
9

You can also define filter number, this filter will automatically parse string value to int:

<select ng-model="ctrl.selectedUnitOrdinal" ng-options="unit.id|number as unit.text for unit in ctrl.unitsOptions"></select>


angular.module('filters').filter('number', [function() {
        return function(input) {
            return parseInt(input, 10);
        };
    }]);
zooblin
  • 2,172
  • 2
  • 27
  • 33
9

Just to add Christophe's answear: easiest way to achive and maintaint it is to make a directive:

JS:

.directive('convertToNumber', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ngModel) {
      ngModel.$parsers.push(function(val) {
        //saves integer to model null as null
        return val == null ? null : parseInt(val, 10);
      });
      ngModel.$formatters.push(function(val) {
        //return string for formatter and null as null
        return val == null ? null : '' + val ;
      });
    }
  };
});

Christophe's answear wont work correctly for '0' as it returns false on "val?" test.

s3r3k
  • 91
  • 1
  • 4
8

You can use ng-value attribute. Like this:

<select ng-model="model.id">
  <option ng-value="0">Zero</option>
  <option ng-value="1">One</option>
  <option ng-value="2">Two</option>
</select>
7

I think the other solutions are overly complex for simply establishing the value as an integer. I would use:

<select ng-model="model.id">
    <option ng-value="{{ 0 }}">Zero</option>
    <option ng-value="{{ 1 }}">One</option>
    <option ng-value="{{ 2 }}">Two</option>
</select>
Ted Scheckler
  • 1,389
  • 4
  • 16
  • 34
3

Angular is seeing your id property as a string, add quotes:

self.selectedUnitOrdinal = "4";
tymeJV
  • 103,943
  • 14
  • 161
  • 157
3

Very similarly to tymeJV's answer, simple workaround is to convert the default selection number to a string like this:

self.selectedUnitOrdinal = valueThatCameFromServer.toString();

This way, you don't have to hardcode the number, you can just convert any received value. It's an easy fix without any filters or directives.

emil.c
  • 1,987
  • 2
  • 26
  • 46
2
<select ng-model="ctrl.selectedUnitOrdinal" ng-options="+(unit.id) as unit.text for unit in ctrl.unitsOptions"></select>
mangesh
  • 355
  • 4
  • 13
  • 1
    While this code snippet may be the solution, including an explanation really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion – Rahul Gupta Dec 28 '17 at 05:18
  • This is what fixed my error - thanks for this. I would suggest that the + turns this value into a String. – HerTesla Aug 28 '19 at 16:36
1

Make id property in unitsOptions as number

self.unitsOptions = Object.keys(unitOptionsFromServer).map(function (key) {
        return { id: +key, text: unitOptionsFromServer[key] };
    });

http://jsfiddle.net/htwc10nx/

oadcub
  • 11
  • 2
0

While the solution provided by Mathew Berg works it will probably break other selects that use sting-valued select options. This may be a problem if you try to write one universal directive for handling selects (like I did).

A nice workaround is to check if the value is a number (even if it is a string containing a number) and only than do the conversion, like so:

angular.module('<module name>').filter('intToString', [function() {
        return function(key) {
            return (!isNaN(key)) ? parseInt(key) : key;
        };
    }]);

or as a controller method:

$scope.intToString = function(key) {
    return (!isNaN(key)) ? parseInt(key) : key;
};
bwitkowicz
  • 779
  • 6
  • 15