Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

feat(ngOptions): add $value variable for easier use of trackBy+selectAs #15584

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 16 additions & 11 deletions src/ng/directive/ngOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ var ngOptionsMinErr = minErr('ngOptions');
*
* ### `select` **`as`** and **`track by`**
*
* <div class="alert alert-warning">
* Be careful when using `select` **`as`** and **`track by`** in the same expression.
* </div>
* When using `select` **`as`** and **`track by`** in the same expression use the `$value` variable.
*
* Given this array of items on the $scope:
*
Expand Down Expand Up @@ -110,6 +108,15 @@ var ngOptionsMinErr = minErr('ngOptions');
* expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value
* is not matched against any `<option>` and the `<select>` appears as having no selected value.
*
* The solution is to use `$value` variable which provides uniform access to each `item` and
* `ngModel` value. Here is the fixed version of the broken example above.
*
* ```html
* <select ng-options="item.subItem as item.label for item in items track by $value.id" ng-model="selected"></select>
* ```
* ```js
* $scope.selected = $scope.items[0].subItem;
* ```
*
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {comprehension_expression} ngOptions in one of the following forms:
Expand Down Expand Up @@ -285,7 +292,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
function(value, locals) { return trackByFn(scope, locals); } :
function getHashOfValue(value) { return hashKey(value); };
var getTrackByValue = function(value, key) {
return getTrackByValueFn(value, getLocals(value, key));
return getTrackByValueFn(value, getLocals(value, key, true));
};

var displayFn = $parse(match[2] || match[1]);
Expand All @@ -294,12 +301,10 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var valuesFn = $parse(match[8]);

var locals = {};
var getLocals = keyName ? function(value, key) {
locals[keyName] = key;
locals[valueName] = value;
return locals;
} : function(value) {
var getLocals = function(value, key, isViewValue) {
if (keyName) locals[keyName] = key;
locals[valueName] = value;
locals['$value'] = isViewValue ? value : viewValueFn(value, locals);
return locals;
};

Expand Down Expand Up @@ -345,7 +350,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
var value = optionValues[key];

var locals = getLocals(value, key);
var locals = getLocals(value, key, true);
var selectValue = getTrackByValueFn(value, locals);
watchedArray.push(selectValue);

Expand Down Expand Up @@ -378,7 +383,7 @@ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile,
for (var index = 0; index < optionValuesLength; index++) {
var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
var value = optionValues[key];
var locals = getLocals(value, key);
var locals = getLocals(value, key, false);
var viewValue = viewValueFn(scope, locals);
var selectValue = getTrackByValueFn(viewValue, locals);
var label = displayFn(scope, locals);
Expand Down
20 changes: 8 additions & 12 deletions test/ng/directive/ngOptionsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1509,22 +1509,18 @@ describe('ngOptions', function() {
});


/**
* This behavior is broken and should probably be cleaned up later as track by and select as
* aren't compatible.
*/
describe('selectAs+trackBy expression', function() {
beforeEach(function() {
scope.arr = [{subItem: {label: 'ten', id: 10}}, {subItem: {label: 'twenty', id: 20}}];
scope.obj = {'10': {subItem: {id: 10, label: 'ten'}}, '20': {subItem: {id: 20, label: 'twenty'}}};
});


it('It should use the "value" variable to represent items in the array as well as for the ' +
it('It should use the "$value" variable to represent items in the array as well as for the ' +
'selected values in track by expression (single&array)', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)'
'ng-options': 'item.subItem as item.subItem.label for item in arr track by $value.id'
});

// First test model -> view
Expand Down Expand Up @@ -1558,12 +1554,12 @@ describe('ngOptions', function() {
});


it('It should use the "value" variable to represent items in the array as well as for the ' +
it('It should use the "$value" variable to represent items in the array as well as for the ' +
'selected values in track by expression (multiple&array)', function() {
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item.subItem as item.subItem.label for item in arr track by (item.id || item.subItem.id)'
'ng-options': 'item.subItem as item.subItem.label for item in arr track by $value.id'
});

// First test model -> view
Expand Down Expand Up @@ -1599,12 +1595,12 @@ describe('ngOptions', function() {
});


it('It should use the "value" variable to represent items in the array as well as for the ' +
it('It should use the "$value" variable to represent items in the array as well as for the ' +
'selected values in track by expression (multiple&object)', function() {
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)'
'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by $value.id'
});

// First test model -> view
Expand Down Expand Up @@ -1644,11 +1640,11 @@ describe('ngOptions', function() {
});


it('It should use the "value" variable to represent items in the array as well as for the ' +
it('It should use the "$value" variable to represent items in the array as well as for the ' +
'selected values in track by expression (single&object)', function() {
createSelect({
'ng-model': 'selected',
'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by (val.id || val.subItem.id)'
'ng-options': 'val.subItem as val.subItem.label for (key, val) in obj track by $value.id'
});

// First test model -> view
Expand Down