fix(select): add basic track by and select as support

Instead of throwing an error when using "track by" and "select as" expressions,
ngOptions will assume that the track by expression is valid, and will use it to
compare values.

Closes #6564
This commit is contained in:
Jeff Cross
2014-10-09 16:48:56 -07:00
parent 6e4955a308
commit addfff3c46
3 changed files with 176 additions and 21 deletions

View File

@@ -3,6 +3,9 @@
@fullName Comprehension expression cannot contain both `select as` and `track by` expressions.
@description
NOTE: This error was introduced in 1.3.0-rc.5, and was removed for 1.3.0-rc.6 in order to
not break existing apps.
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.

View File

@@ -363,13 +363,6 @@ 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);
@@ -460,7 +453,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
function createIsSelectedFn(viewValue) {
var selectedSet;
if (multiple) {
if (!selectAs && trackFn && isArray(viewValue)) {
if (trackFn && isArray(viewValue)) {
selectedSet = new HashMap([]);
for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) {
@@ -470,15 +463,16 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
} else {
selectedSet = new HashMap(viewValue);
}
} else if (!selectAsFn && trackFn) {
} else if (trackFn) {
viewValue = callExpression(trackFn, null, viewValue);
}
return function isSelected(key, value) {
var compareValueFn;
if (selectAsFn) {
compareValueFn = selectAsFn;
} else if (trackFn) {
if (trackFn) {
compareValueFn = trackFn;
} else if (selectAsFn) {
compareValueFn = selectAsFn;
} else {
compareValueFn = valueFn;
}

View File

@@ -817,22 +817,180 @@ describe('select', 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 = [{id: 10, label: 'ten'}, {id:'20', label: 'twenty'}];
scope.obj = {'10': {score: 10, label: 'ten'}, '20': {score: 20, label: 'twenty'}};
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('should throw a helpful minerr', function() {
expect(function() {
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)'
});
createSelect({
'ng-model': 'selected',
'ng-options': 'item.id as item.name for item in values track by item.id'
});
// First test model -> view
}).toThrowMinErr('ngOptions', 'trkslct', "Comprehension expression cannot contain both selectAs 'item.id' and trackBy 'item.id' expressions.");
scope.$apply(function() {
scope.selected = scope.arr[0].subItem;
});
expect(element.val()).toEqual('0');
scope.$apply(function() {
scope.selected = scope.arr[1].subItem;
});
expect(element.val()).toEqual('1');
// Now test view -> model
element.val('0');
browserTrigger(element, 'change');
expect(scope.selected).toBe(scope.arr[0].subItem);
// Now reload the array
scope.$apply(function() {
scope.arr = [{
subItem: {label: 'new ten', id: 10}
},{
subItem: {label: 'new twenty', id: 20}
}];
});
expect(element.val()).toBe('0');
expect(scope.selected.id).toBe(10);
});
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)'
});
// First test model -> view
scope.$apply(function() {
scope.selected = [scope.arr[0].subItem];
});
expect(element.val()).toEqual(['0']);
scope.$apply(function() {
scope.selected = [scope.arr[1].subItem];
});
expect(element.val()).toEqual(['1']);
// Now test view -> model
element.find('option')[0].selected = true;
element.find('option')[1].selected = false;
browserTrigger(element, 'change');
expect(scope.selected).toEqual([scope.arr[0].subItem]);
// Now reload the array
scope.$apply(function() {
scope.arr = [{
subItem: {label: 'new ten', id: 10}
},{
subItem: {label: 'new twenty', id: 20}
}];
});
expect(element.val()).toEqual(['0']);
expect(scope.selected[0].id).toEqual(10);
expect(scope.selected.length).toBe(1);
});
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)'
});
// First test model -> view
scope.$apply(function() {
scope.selected = [scope.obj['10'].subItem];
});
expect(element.val()).toEqual(['10']);
scope.$apply(function() {
scope.selected = [scope.obj['10'].subItem];
});
expect(element.val()).toEqual(['10']);
// Now test view -> model
element.find('option')[0].selected = true;
element.find('option')[1].selected = false;
browserTrigger(element, 'change');
expect(scope.selected).toEqual([scope.obj['10'].subItem]);
// Now reload the object
scope.$apply(function() {
scope.obj = {
'10': {
subItem: {label: 'new ten', id: 10}
},
'20': {
subItem: {label: 'new twenty', id: 20}
}
};
});
expect(element.val()).toEqual(['10']);
expect(scope.selected[0].id).toBe(10);
expect(scope.selected.length).toBe(1);
});
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)'
});
// First test model -> view
scope.$apply(function() {
scope.selected = scope.obj['10'].subItem;
});
expect(element.val()).toEqual('10');
scope.$apply(function() {
scope.selected = scope.obj['10'].subItem;
});
expect(element.val()).toEqual('10');
// Now test view -> model
element.find('option')[0].selected = true;
browserTrigger(element, 'change');
expect(scope.selected).toEqual(scope.obj['10'].subItem);
// Now reload the object
scope.$apply(function() {
scope.obj = {
'10': {
subItem: {label: 'new ten', id: 10}
},
'20': {
subItem: {label: 'new twenty', id: 20}
}
};
});
expect(element.val()).toEqual('10');
expect(scope.selected.id).toBe(10);
});
});