Last week I upgraded an existing Angular application from 1.3 to 1.4rc. For the most part this migration was smooth, but I ran into one subtle change that bit me and took a while to track down. Angular 1.4 changes the behavior of <SELECT> element binding when binding static items. Specifically 1.4 and forward requires that the type of values bound matches the type of the actual values that are attached to each option. In other words if you have a numeric type and you’re binding to static option values (which are strings) the binding will fail to show the proper result value.
To make this clearer lets look at an example. In the Westwind.Globalization Localization form I have a simple form where I can select the type of resource that I’m localizing for. I have 3 fixed values in that dropdown as you can see in this screenshot:
In the application, the dropdown list is bound to the model via view.activeResource.ValueType which is a numeric value.
In Angular 1.3 I was simply able to bind these static values using ng-model like this:
<select class="form-control-small"
ng-model="view.activeResource.ValueType">
<option value="0">Text</option>
<option value="2" >Markdown</option>
<option value="1">Binary</option>
</select>
and this worked just fine. When initially displaying the page the model value was pre-selected in the list and when I made a selection the ValueType property on the model was updated. Life was good.
Angular 1.4+: Binding Type Changes
Starting with Angular 1.4 the binding behavior is changing in that bindings must match their types exactly to the model value types. In Angular 1.3 the binding logic was a bit more loose so comparing a string value and a numeric value would result in equality. In Angular 1.4 this loose comparison is no longer used and values have to match exactly.
This is a problem if you are using static values that are assigned in HTML and which are always string values, but when your model’s values are non string values. In the example above, I’m binding to static HTML option values that are strings from a model that is using a numeric value.
This causes two problems:
- On inbound binding the model value always selects the first item
(ie. the initial value never binds properly so the first item is selected)
- On outbound binding the model is bound with a string value rather than a number
(ie. after selection the value is invalid)
The outbound issue in turn can cause problems when taking the data and pushing it back to the server to save. Since the server is expecting a numeric value this causes either a JSON unbinding error, or – more likely – the value is ignored and the default value is used instead (0 – Text) regardless of what the user selected.
More heinously though – the UI displays the selection correctly. Until the data is refreshed from the server – an incorrect out of sync value is actually displayed which made this doubly difficult to track down.
If you’re updating an existing application like I did this is a very subtle change – this code used to work fine in 1.3 and prior. Now in 1.4 this binding as is, is broken and it’s a subtle thing to detect.
Note: This doesn’t affect all Bindings
To be clear this is a very specific problem that occurs only if you are binding a non-string model value to a static list of values that you hardcode in HTML, which is probably not all that often.
This is not a problem if you are:
- Binding string values – since the values are strings anyway
- Binding dynamic binding values that you assign via ng-repeat/ng-option
Most of the time we bind dynamic data, and if you do that you are likely binding the proper value types that Angular knows about. The most common scenario is exactly like I describe above where Angular is not used to populate the list data, but you are binding a non-string value in ng-model.
Fixing the Problem with a tiny Angular Directive
When I ran into this problem initially, I created an issue in the Angular Github repository, and what follows was the suggestion for the proper way to bind static values which involves using a custom directive. In fact, the Angular documentation was updated as a result of this report – nice.
So there are a number of ways to address this problem, but after playing around with a few of them the easiest and most reusable solution is to use a custom a number conversion directive as recommended in the bug report.
The following is a convert-to-number directive which essentially parses numeric values to strings for binding and strings to numbers on unbinding:
app.directive('convertToNumber', function() {
return {
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$parsers.push(function(val) {
return parseInt(val, 10);
});
ngModel.$formatters.push(function (val) {
return '' + val;
});
}
};
});
To use this directive, you simply add it to the <select> control like this:
<select class="form-control-small"
ng-model="view.activeResource.ValueType"
convert-to-number>
<option value="0">Text</option>
<option value="2" >Markdown</option>
<option value="1">Binary</option>
</select>
And voila, it works. I now get my list pre-selected again on inbound binding, and my result value after a selection is a number.
Summary
While the solution to this problem is simple enough, this is one of those things that are very difficult to detect and figure out. And even once you figure it out, how the heck do you arrive at the workaround for this? I applaud the Angular team for responding very quickly to the bug report I posted and immediately adding the workaround to the Angular Select documentation which is great. I still wonder though whether I would have thought of looking there to find the workaround if I ran into this.
Again, this is kind of an edge case but I know I have quite a few forms and pages with static selection lists where this issue might crop up. I’m bound to forget so writing it down here might jog my memory in the future or at least let me find it when I can’t remember that I did…
Related Links
Other Posts you might also like