mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
fix(select): throw for selectAs and trackBy
trackBy and selectAs have never worked together, and are fundamentally incompatible since model changes cannot deterministically be reflected back to the view. This change throws an error to help developers better understand this scenario.
This commit is contained in:
30
docs/content/error/ngOptions/trkslct.ngdoc
Normal file
30
docs/content/error/ngOptions/trkslct.ngdoc
Normal file
@@ -0,0 +1,30 @@
|
||||
@ngdoc error
|
||||
@name ngOptions:trkslct
|
||||
@fullName Comprehension expression cannot contain both `select as` and `track by` expressions.
|
||||
@description
|
||||
|
||||
This error occurs when 'ngOptions' is passed a comprehension expression that contains both a
|
||||
`select as` expression and a `track by` expression. These two expressions are fundamentally
|
||||
incompatible.
|
||||
|
||||
* Example of bad expression: `<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 always applied to `value`, with purpose to preserve the selection,
|
||||
(to `item` in this case)
|
||||
* To calculate whether an item is selected, `ngOptions` does the following:
|
||||
1. apply `track by` to the values in the array:
|
||||
In the example: [1,2]
|
||||
2. apply `track by` to the already selected value in `ngModel`:
|
||||
In the example: this is not possible, as `track by` refers to `item.id`, but the selected
|
||||
value from `ngModel` is `{name: aSubItem}`.
|
||||
|
||||
Here's an example of how to make this example work by using `track by` without `select as`:
|
||||
|
||||
```
|
||||
<select ng-model="selected" ng-options="item.label for item in values track by item.id">
|
||||
```
|
||||
|
||||
Note: This would store the whole `item` as the model to `scope.selected` instead of `item.subItem`.
|
||||
|
||||
For more information on valid expression syntax, see 'ngOptions' in {@link ng.directive:select select} directive docs.
|
||||
@@ -36,8 +36,8 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* </div>
|
||||
*
|
||||
* <div class="alert alert-info">
|
||||
* **Note:** Using `selectAs` will bind the result of the `selectAs` expression to the model, but
|
||||
* the value of the `select` and `option` elements will be either the index (for array data sources)
|
||||
* **Note:** Using `select as` will bind the result of the `select as` expression to the model, but
|
||||
* the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
|
||||
* or property name (for object data sources) of the value within the collection.
|
||||
* </div>
|
||||
*
|
||||
@@ -79,19 +79,18 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* even when the options are recreated (e.g. reloaded from the server).
|
||||
|
||||
* <div class="alert alert-info">
|
||||
* **Note:** Using `selectAs` together with `trackexpr` is not possible (and will throw).
|
||||
* TODO: Add some nice reasoning here, add a minErr and a nice error page.
|
||||
* reasoning:
|
||||
* **Note:** Using `select as` together with `trackexpr` is not possible (and will throw).
|
||||
* Reasoning:
|
||||
* - Example: <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'};
|
||||
* - trackBy is always applied to `value`, with purpose to preserve the selection,
|
||||
* - track by is always applied to `value`, with purpose to preserve the selection,
|
||||
* (to `item` in this case)
|
||||
* - to calculate whether an item is selected we do the following:
|
||||
* 1. apply `trackBy` to the values in the array, e.g.
|
||||
* 1. apply `track by` to the values in the array, e.g.
|
||||
* In the example: [1,2]
|
||||
* 2. apply `trackBy` to the already selected value in `ngModel`:
|
||||
* In the example: this is not possible, as `trackBy` refers to `item.id`, but the selected
|
||||
* 2. apply `track by` to the already selected value in `ngModel`:
|
||||
* In the example: this is not possible, as `track by` refers to `item.id`, but the selected
|
||||
* value from `ngModel` is `{name: aSubItem}`.
|
||||
*
|
||||
* </div>
|
||||
@@ -367,6 +366,13 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
//re-usable object to represent option's locals
|
||||
locals = {};
|
||||
|
||||
if (trackFn && selectAsFn) {
|
||||
throw ngOptionsMinErr('trkslct',
|
||||
"Comprehension expression cannot contain both selectAs '{0}' " +
|
||||
"and trackBy '{1}' expressions.",
|
||||
selectAs, track);
|
||||
}
|
||||
|
||||
if (nullOption) {
|
||||
// compile the element since there might be bindings in it
|
||||
$compile(nullOption)(scope);
|
||||
@@ -485,7 +491,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
} else {
|
||||
return viewValue == callExpression(compareValueFn, key, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleRendering() {
|
||||
|
||||
@@ -663,7 +663,7 @@ describe('select', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('trackBy', function() {
|
||||
describe('trackBy expression', function() {
|
||||
beforeEach(function() {
|
||||
scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];
|
||||
scope.obj = {'10': {score: 10, label: 'ten'}, '20': {score: 20, label: 'twenty'}};
|
||||
@@ -761,173 +761,22 @@ describe('select', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('selectAs+trackBy', function() {
|
||||
describe('selectAs+trackBy expression', function() {
|
||||
beforeEach(function() {
|
||||
scope.arr = [{id: 10, label: 'ten'}, {id:'20', label: 'twenty'}];
|
||||
scope.obj = {'10': {score: 10, label: 'ten'}, '20': {score: 20, label: 'twenty'}};
|
||||
});
|
||||
|
||||
|
||||
it('should bind selectAs expression result to scope (array&single)', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'ng-options': 'item.id as item.name for item in values track by item.id'
|
||||
});
|
||||
it('should throw a helpful minerr', function() {
|
||||
expect(function() {
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.values = [{id: 10, name: 'A'}, {id: 20, name: 'B'}];
|
||||
scope.selected = 10;
|
||||
});
|
||||
expect(element.val()).toEqual('0');
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'ng-options': 'item.id as item.name for item in values track by item.id'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = 20;
|
||||
});
|
||||
expect(element.val()).toEqual('1');
|
||||
|
||||
element.val('0');
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toBe(10);
|
||||
});
|
||||
|
||||
|
||||
it('should bind selectAs expression result to scope (array&multiple)',function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'multiple': true,
|
||||
'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 = [10];
|
||||
});
|
||||
expect(element.val()).toEqual(['0']);
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = [20];
|
||||
});
|
||||
expect(element.val()).toEqual(['1']);
|
||||
|
||||
element.children(0).attr('selected', 'selected');
|
||||
element.children(1).attr('selected', 'selected');
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toEqual([10, 20]);
|
||||
});
|
||||
|
||||
|
||||
it('should bind selectAs expression result to scope (object&single)', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'ng-options': 'value.score as value.label for (key, value) in obj track by value.score'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = 10;
|
||||
});
|
||||
expect(element.val()).toEqual('10');
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = 20;
|
||||
});
|
||||
expect(element.val()).toEqual('20');
|
||||
|
||||
element.val('10');
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toBe(10);
|
||||
});
|
||||
|
||||
|
||||
it('should bind selectAs expression result to scope (object&multiple)', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'multiple': true,
|
||||
'ng-options': 'value.score as value.label for (key, value) in obj track by value.score'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = [10];
|
||||
});
|
||||
expect(element.val()).toEqual(['10']);
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = [20];
|
||||
});
|
||||
expect(element.val()).toEqual(['20']);
|
||||
|
||||
element.find('option')[0].selected = 'selected';
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toEqual([10, 20]);
|
||||
});
|
||||
|
||||
|
||||
it('should correctly assign model if track & select expressions differ (array&single)', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'ng-options': 'item.label as item.label for item in arr track by item.id'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = 'ten';
|
||||
});
|
||||
expect(element.val()).toBe('0');
|
||||
|
||||
element.val('1');
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toBe('twenty');
|
||||
});
|
||||
|
||||
|
||||
it('should correctly assign model if track & select expressions differ (array&multiple)', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'multiple': true,
|
||||
'ng-options': 'item.label as item.label for item in arr track by item.id'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = ['ten'];
|
||||
});
|
||||
expect(element.val()).toEqual(['0']);
|
||||
|
||||
element.find('option')[1].selected = 'selected';
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toEqual(['ten', 'twenty']);
|
||||
});
|
||||
|
||||
|
||||
it('should correctly assign model if track & select expressions differ (object&single)', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'ng-options': 'val.label as val.label for (key, val) in obj track by val.score'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = 'ten';
|
||||
});
|
||||
expect(element.val()).toBe('10');
|
||||
|
||||
element.val('20');
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toBe('twenty');
|
||||
});
|
||||
|
||||
|
||||
it('should correctly assign model if track & select expressions differ (object&multiple)', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'multiple': true,
|
||||
'ng-options': 'val.label as val.label for (key, val) in obj track by val.score'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = ['ten'];
|
||||
});
|
||||
expect(element.val()).toEqual(['10']);
|
||||
|
||||
element.find('option')[1].selected = 'selected';
|
||||
browserTrigger(element, 'change');
|
||||
expect(scope.selected).toEqual(['ten', 'twenty']);
|
||||
}).toThrowMinErr('ngOptions', 'trkslct', "Comprehension expression cannot contain both selectAs 'item.id' and trackBy 'item.id' expressions.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1264,7 +1113,7 @@ describe('select', function() {
|
||||
it('should bind to scope value and track/identify objects', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
'ng-options': 'item as item.name for item in values track by item.id'
|
||||
'ng-options': 'item.name for item in values track by item.id'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
|
||||
Reference in New Issue
Block a user