When both a track by expression and a select as expression are used in ng-options, binding doesn't work anymore.
For instance:
it('should bind to scope value through experession while tracking/identifying objects', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.id as item.name for item in values track by item.id'
});
scope.$apply(function() {
scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
scope.selected = scope.values[0].id;
});
expect(element.val()).toEqual('10');
scope.$apply(function() {
scope.selected = scope.values[1].id;
});
expect(element.val()).toEqual('20');
});
It seems that in the ngOptions directive, the track function always directly use the value expression even if a select as expression is set.
Oops, accidentally deleted my previous comment.
I created/modified a fiddle to illustrate the problem:
http://jsfiddle.net/qWzTb/418/
+1 I'm having the same problem.
It's annoying but you can really just leave away the track by
and it'll work.
Can confirm this problem still exists as of v1.3.0-beta.15.
Having the same problem with 1.2.20 :/
Same here
I've been encountering this again.
The problem was created in c32a859bdb93699cc080f9affed4bcff63005a64 by @quazzie:
For checking if an option is selected, the trackFn
is used, where the valueFn
should be used instead. I'll try to come up with a fix.
Ok, I have investigated this. It's not actually a bug, just misleading documentation.
The thing is, you just can't combine value as label for collection
with track by
.
You have to pick the one or the other. There are two different Use Cases:
Use value as label
:
items = [
{value: 1, label: 'One'},
{value: 2, label: 'Two'},
]
template = 'ng-options="item.value as item.label for items"'
When you change the dropdown, the model is assigned whatever you define in front of the as
.
Use track by
:
items = [
{id: 1, ...},
{id: 2, ...},
{id: 3, ...},
]
template = 'ng-options="item as item.label for items track by items.id"'
When you change the dropdown, the model is assigned the item. The track by
expression is only used to associate options with items through the value
attribute on the <option>
.
Illustrated in http://jsfiddle.net/0vpsv1wa/1/
Faced the same problem, and figured a workaround, but I don't know if this is going to solve future issues (at least in the project I'm doing this).
Somewhere on your code add this ugly script.
$(document).on('change', 'select', function(e){
var scope = angular.element($(this)).scope();
var val = scope[$(this).attr('name')];
$(this).val(val);
});
Interestingly, if I remove track-by
from the original test, the expectations still fail:
'ng-options': 'item.id as item.name for item in values'
Expected '0' to equal '10
Expected '1' to equal '20'
@jeffbcross: This is "expected", since with ngOptions
the select will use the option's index as value (for the <option>
element) and not for example item.id
.
This is understandable, since the modelValue could be an object.
The test should rather be:
it('should bind to scope value through experession while tracking/identifying objects', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.id as item.name for item in values track by item.id'
});
var selectedIdx;
scope.$apply(function() {
scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
selectedIdx = 0;
scope.selected = scope.values[selectedIdx].id;
});
expect(element.val()).toEqual('' + selectedIdx);
scope.$apply(function() {
selectedIdx = 1;
scope.selected = scope.values[selectedIdx].id;
});
expect(element.val()).toEqual('' + selectedIdx);
});
(And removing track by ...
makes it pass as expected.)
But @gkalpak what is the point of using select as
expression if select just uses the index of the option as the value anyway? @IgorMinar and I have been looking through tests and implementation of this and are a little confused. It seems like select as
isn't really working as documented. I would expect this:
scope.$apply(function() {
scope.values = [{id: 10, name: 'A'}];
scope.selected = 10;
});
expect(element.val()).toBe('10');
Am I misunderstanding how select as
is supposed to work?
@jeffbcross: If I am not mistaken, the point of select as
is to be able to have modelValues that are not restricted to being strings (as is the case when using "tranditional" <option>
elements ("by hand" or via ngRepeat
)).
So, the value of the <option>
elements are indices (in order to ensure that they can be represented as strings) and the specified select
value is bound to the modelValue (if there ngModel
is present).
E.g.:
<select ng-model="someObject" ng-options="item as item.label for item in items"></select>
// with:
$scope.items = [
{id: 1, label: 'Item 1', someProp: 'someValue1', someFunc: function () {}},
{id: 2, label: 'Item 2', someProp: 'someValue2', someFunc: function () {}},
....
];
will:
Item 1
, Item 2
, ... as <option>
textContent0
, 1
, ... as <option>
values$scope.someObject
with the item
corresponding to the selected <option>
(i.e. index)This way, it is possible to assign "non-stringifiable" objects to the model, while still having unique, distinguishable values for the <option>
elements.
@gkalpak thanks for the detail. Your expression behaves exactly the same way if you would remove the item as
part, because ngOptions
by default uses value
as select
.
it('should give index of object as value', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.label for item in values'
});
scope.$apply(function() {
scope.values = [{label: 'foo'}];
scope.selected = scope.values[0];
});
expect(element.val()).toBe('0');
expect(element.children().eq(0).text()).toBe('foo');
expect(scope.selected).toBe(scope.values[0]);
});
@jeffbcross : Indeed. Just to be clear though, you don't have to use the item itself. You can use a property of the item, which can be an object also. E.g.:
<select ng-model="someObject" ng-options="item.prop as item.label for item in items"></select>
// with:
$scope.items = [
{id: 1, label: 'Item 1', prop: {key1: 'val1.1', key2: 'val1.2'}},
{id: 2, label: 'Item 2', prop: {key1: 'val2.1', key2: 'val2.2'}},
....
];
Ahh, I see. Thanks for the clarification. We should document that better :). Unless someone (@IgorMinar) has a valid use case for element.val()
needing to return the actual expression result, we should probably not change anything with select as
.
I'll focus back on finding the source of the original issue now...
Hi,
Is this will be fixed in the next release?
<select ng-model="company.user_id" ng-options="(person.first_name +' '+ person.last_name) for person in salesperson track by person.id"></select>
$scope.salesperson = [
{
id: 5,
first_name: 'John',
last_name: 'Doe'
},
{
id: 10,
first_name: 'Jane',
last_name: 'Doe'
}
];
$scope.company.user_id = 10; // OK but the UI isn't updated (v1.3.0-rc.4)
$scope.company.user_id = $scope.salesperson[1]; // OK but needs a manual loop to retrieve '1'
Bests
Here is a WIP build with a fix for these issues (and some other issues with ngOptions): https://gist.github.com/jeffbcross/7c3e7efa22283bfc3c1c
I created a plnkr based on @janv's jsfiddle, and the reported issue appears to be fixed.
@janv @damiengenet @SQiShER @ahoereth @wojciechfornal @jsanta @matheusvalim would you mind testing this build with your code to see if the issue is resolved?
We ended up deciding that supporting these two expressions together is not feasible, and so we are throwing an error when used in combination. These expressions are fundamentally incompatible because it is not possible to reliably and consistently determine the parent value of a model, since select as
can assign any child of a value
as the model value.
Prior to refactorings in this release, neither of these expressions worked correctly independently,
and did not work at all when combined. @tbosch and I added many more tests and refactored most of the ngOptions logic in ab354cf04ef8277a87d1ea1e79f9d63876fa97c8 and aad60953ce1c68433b6ae74b6a62a55f80a45e6e. So the good news is, ngOptions
should be working more consistently for everyone now (although this may cause breaking changes since folks probably have code that's expecting broken behavior).
This breaking change was frustrating for me, as I found there were a few places in my code that relied on having different expressions for select as
and track by
. Primarily, this was using the track by
expression to match up between the ng-model
value and the correct option in the list of options (which might have been fetched from the database, for example). Since they were different objects, the track by
enabled Angular to know which option in the list to select.
If other people are as frustrated as me by this change, then the following directive that I created may be helpful for you. Essentially, it enables you to restore that functionality of track by
if you are relying on the select as
in your ng-options
expression by effectively combining the two:
``` javascript:
/**
var trackByFixFnName = $attrs.ngOptionsTrackByFix || 'trackByFix';
if ($scope[trackByFixFnName]) {
throw new Error('There is already a $scope.' + trackByFixFnName + ' defined, you will need to specify ' +
'a different name for the ng-options-track-by-fix attribute and use that name instead of trackByFix.');
}
$scope[trackByFixFnName] = function trackByFix(trackByExpr, option) {
var trackByFn = $parse(trackByExpr),
modelValue = modelValueGetter($scope);
if (trackByFn($scope, getLocals(option)) === trackByFn($scope, getLocals(modelValue)))
return modelValue;
else
return option;
};
function getLocals(value) {
var locals = {};
locals[valueExpression] = value;
return locals;
}
}
};
}]);
```
Hi,
could you give a more concrete example how you used to use track by
together with select as
?
Because I think they are generally not compatible. See this example (from https://github.com/angular/angular.js/blob/master/docs/content/error/ngOptions/trkslct.ngdoc):
<select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">`
values = [{id: 1, label: 'aLabel', subItem: {name: 'aSubItem'}}, {id: 2, label: 'bLabel', subItem: {name: 'bSubItem'}}]`
$scope.selected = {name: 'aSubItem'};
track by
is used to calculate whether an item is selected, even when it was reloaded from the server. This can only be done in the following way:
track by
to the values in the array:track by
to the already selected value in ngModel
:track by
refers to item.id
, but the selectedngModel
is {name: aSubItem}
, which does not include a property id
.Hi @tbosch,
I have constructed a Plunkr example that illustrates the main place I was relying on both select as
and track by
to make things easier.
Essentially, I have an array of options that contains properties that are used for the group by
label, and a value
property which is the object actually bound as the model value.
I would then have a track by
expression that would work for both the subobject and the option in the array.
So the example you cited could be fixed to use this technique to have:
ng-options="item.subItem as item.label for item in values track by (item.subitem.name || item.name)"
.
This should then work correctly.
Is this not a valid use case?
@greglockwood I forked the plnkr and changed the expression to work with rc.5. I just changed ng-model
to to be data
and removed the selectAs. I can imagine use cases of shared models where this could cause some inconvenience though.
<!--Before-->
<select ng-model="data.value" ng-options="opt.value as opt.display group by opt.groupName for opt in options track by (opt.id || opt.value.id)"></select>
<!--After-->
<select ng-model="data" ng-options="opt.display group by opt.groupName for opt in options track by (opt.id || opt.value.id)"></select>
Your track by
expression is one that was select as
-aware and would actually work. Unfortunately, there's no way we can know at runtime if the track expression is compatible with model and option value. It's worth considering something less restrictive than a thrown error, like a "I know what I'm doing" opt-out, or just logging a warning. @tbosch what do you think?
Reopening so we can give this some more consideration. After some discussion with @tbosch and @IgorMinar, here are some proposed approaches to allowing track by
and select as
to work together:
subItem
s, since this is what was set to the model via select as
.$selectedValue
object, which would be the encouraged variable to use in track by
expressions. We could deprecate accessing item
and log a warning when it is accessed, and in a future version we could change this warning to throw an error.Changing the milestone since 1.3.0-rc.5
is already released.
We ended up deciding to just make the old behavior possible. Working on a PR
@jeffbcross This is a tricky one. Whilst I am grateful for undoing the change to make the old behaviour possible, I agree it was confusing. In particular, that you have to construct an expression for the track by
that works for both types of context is not obvious, and was a pain point for me when developing that part of the application I am working on.
In response to your fork of my plnkr, I offer you a counter-fork which demonstrates something closer to my actual use case, in that it is indeed using a "shared model". In such a case, it is not valid to overwrite the parent object with the complete option selected, as the dbObject
and each opt
share a common structure only in terms of the value
property.
As for your proposed solutions, I don't think either of them is the perfect solution, as they both have their downsides.
The first is probably the better of the two, though. Whilst it is a breaking change, it is not a huge deal to cope with, as most people would just find themselves deleting <identifier>.
from their track by expressions to adapt.
My main misgiving about this change is that the context for the track by
expression would be different to the context for the majority of the other subexpressions in the greater ng-options
expression.
Here is a graphic to illustrate what I mean:
As you can see, there would be 4 different contexts within a single ng-options expression with this change:
<select>
, shown in blueIn particular, that the track by
expression is evaluated against two different contexts is reasonably without precedent AFAIK, and potentially confusing.
The cons of the second solution, introducing a $selectedValue
magic value for use in track by
, include:
track by
works with ng-repeat
(this con is shared by the first solution too).$selectedValue
in your track by expression, which effectively makes it redundant.The biggest thing in its favour is that it is an _explicit context_, which is potentially less confusing for people and it is easier to document and explain it as a special "double purpose" context. It would also be fairly easy to migrate to.
Anyways, those are just my thoughts (with input from a talented co-worker).
Thanks for bringing the issue up, and thanks for the feedback on the approaches. This is a tough problem without a clean solution that satisfies all constraints, is consistent with ng-repeat, and is backwards-compatible. In the meantime, I removed the thrown error, added tests for how track by and select as should work together, and fixed the implementation.
Unfortunately, the originally reported issue would not work with the expression/model as is, but could work with some re-working.
This is still happening in v1.4.7, no fix yet?
same problem here!!
Same problem!!
same thing here with in v1.4.7
Same thing with v1.4.8
Same thing with v1.5.3
The ng-options works fine, but when update the model manual, the select value hadn't update.
<select ng-model="shiny"
ng-options="opt.value as opt.label for opt in options">
</select>
$scope.complex = 2;
There are certain (documented) limitations in using track by
together with select ... as
.
Not all usecases are supported.
ng-options="item.id as item.name for item in values track by item.id" works, but doesn't se the default value
http://plnkr.co/edit/6YdeJviPnCwY3Em9ltyo?p=preview
No, it doesn't work (i.e. track by has no effect).
is there a solution to this yet?
https://stackoverflow.com/questions/44629489/malfunctioning-select-option-dropdowns-while-using-ngrepeat-or-ngoptions-ngmo
Most helpful comment
Ok, I have investigated this. It's not actually a bug, just misleading documentation.
The thing is, you just can't combine
value as label for collection
withtrack by
.You have to pick the one or the other. There are two different Use Cases:
1. You want a shiny presentation for an ugly value
Use
value as label
:When you change the dropdown, the model is assigned whatever you define in front of the
as
.2. You want to iterate over complex objects
Use
track by
:When you change the dropdown, the model is assigned the item. The
track by
expression is only used to associate options with items through thevalue
attribute on the<option>
.Illustrated in http://jsfiddle.net/0vpsv1wa/1/