diff --git a/src/ng/directive/ngOptions.js b/src/ng/directive/ngOptions.js index af963c665dc7..ee7cf32831f9 100644 --- a/src/ng/directive/ngOptions.js +++ b/src/ng/directive/ngOptions.js @@ -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: * @@ -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: @@ -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]); @@ -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; }; @@ -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); @@ -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); diff --git a/test/ng/directive/ngOptionsSpec.js b/test/ng/directive/ngOptionsSpec.js index 56b11d04f0d2..b7845b31a016 100644 --- a/test/ng/directive/ngOptionsSpec.js +++ b/test/ng/directive/ngOptionsSpec.js @@ -1509,10 +1509,6 @@ 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}}]; @@ -1520,11 +1516,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&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 @@ -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 @@ -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 @@ -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