mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
feat(select): match options by expression other than object identity
Extend ng-options with a new clause, "track by [trackByExpression]", which can be used when
working with objects. The `trackByExpression` should uniquely identify select options objects.
This solves the problem of previously having to match ng-options objects by identity.
You can now write: `ng-options="obj as obj.name for obj in objects track by obj.id"`
The "track by" expression will be used when checking for equality of objects.
Examples:
<select
ng-model="user.favMovieStub"
ng-options="movie as movie.name for movie in movies track by movie.id">
</select>
scope: {
user: { name: 'Test user', favMovieStub: { id: 1, name: 'Starwars' } }
movies: [{ id: 1, name: 'Starwars', rating: 5, ... }, { id: 13, ... }]
}
The select input will match user favMovieStub to the first movie in the movies array, and show
"Star Wars" as the selected item.
This commit is contained in:
parent
4acc28a310
commit
c32a859bdb
2 changed files with 80 additions and 13 deletions
|
|
@ -39,7 +39,7 @@
|
|||
* * `label` **`for`** `value` **`in`** `array`
|
||||
* * `select` **`as`** `label` **`for`** `value` **`in`** `array`
|
||||
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
|
||||
* * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
|
||||
* * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
|
||||
* * for object data sources:
|
||||
* * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
||||
* * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
||||
|
|
@ -59,6 +59,9 @@
|
|||
* element. If not specified, `select` expression will default to `value`.
|
||||
* * `group`: The result of this expression will be used to group options using the `<optgroup>`
|
||||
* DOM element.
|
||||
* * `trackexpr`: Used when working with an array of objects. The result of this expression will be
|
||||
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
|
||||
* `value` variable (e.g. `value.propertyName`).
|
||||
*
|
||||
* @example
|
||||
<doc:example>
|
||||
|
|
@ -123,8 +126,8 @@
|
|||
|
||||
var ngOptionsDirective = valueFn({ terminal: true });
|
||||
var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
//0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000077770
|
||||
var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/,
|
||||
//0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000007777000000000000000000088888
|
||||
var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/,
|
||||
nullModelCtrl = {$setViewValue: noop};
|
||||
|
||||
return {
|
||||
|
|
@ -298,7 +301,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
|||
|
||||
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
|
||||
throw Error(
|
||||
"Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
|
||||
"Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_ (track by _expr_)?'" +
|
||||
" but got '" + optionsExp + "'.");
|
||||
}
|
||||
|
||||
|
|
@ -308,6 +311,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
|||
groupByFn = $parse(match[3] || ''),
|
||||
valueFn = $parse(match[2] ? match[1] : valueName),
|
||||
valuesFn = $parse(match[7]),
|
||||
track = match[8],
|
||||
trackFn = track ? $parse(match[8]) : null,
|
||||
// This is an array of array of existing option groups in DOM. We try to reuse these if possible
|
||||
// optionGroupsCache[0] is the options with no option group
|
||||
// optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
|
||||
|
|
@ -348,7 +353,14 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
|||
if ((optionElement = optionGroup[index].element)[0].selected) {
|
||||
key = optionElement.val();
|
||||
if (keyName) locals[keyName] = key;
|
||||
locals[valueName] = collection[key];
|
||||
if (trackFn) {
|
||||
for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) {
|
||||
locals[valueName] = collection[trackIndex];
|
||||
if (trackFn(scope, locals) == key) break;
|
||||
}
|
||||
} else {
|
||||
locals[valueName] = collection[key];
|
||||
}
|
||||
value.push(valueFn(scope, locals));
|
||||
}
|
||||
}
|
||||
|
|
@ -360,9 +372,19 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
|||
} else if (key == ''){
|
||||
value = null;
|
||||
} else {
|
||||
locals[valueName] = collection[key];
|
||||
if (keyName) locals[keyName] = key;
|
||||
value = valueFn(scope, locals);
|
||||
if (trackFn) {
|
||||
for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) {
|
||||
locals[valueName] = collection[trackIndex];
|
||||
if (trackFn(scope, locals) == key) {
|
||||
value = valueFn(scope, locals);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
locals[valueName] = collection[key];
|
||||
if (keyName) locals[keyName] = key;
|
||||
value = valueFn(scope, locals);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctrl.$setViewValue(value);
|
||||
|
|
@ -394,7 +416,15 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
|||
label;
|
||||
|
||||
if (multiple) {
|
||||
selectedSet = new HashMap(modelValue);
|
||||
if (trackFn && isArray(modelValue)) {
|
||||
selectedSet = new HashMap([]);
|
||||
for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) {
|
||||
locals[valueName] = modelValue[trackIndex];
|
||||
selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]);
|
||||
}
|
||||
} else {
|
||||
selectedSet = new HashMap(modelValue);
|
||||
}
|
||||
}
|
||||
|
||||
// We now build up the list of options we need (we merge later)
|
||||
|
|
@ -406,15 +436,21 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
|||
optionGroupNames.push(optionGroupName);
|
||||
}
|
||||
if (multiple) {
|
||||
selected = selectedSet.remove(valueFn(scope, locals)) != undefined;
|
||||
selected = selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) != undefined;
|
||||
} else {
|
||||
selected = modelValue === valueFn(scope, locals);
|
||||
if (trackFn) {
|
||||
var modelCast = {};
|
||||
modelCast[valueName] = modelValue;
|
||||
selected = trackFn(scope, modelCast) === trackFn(scope, locals);
|
||||
} else {
|
||||
selected = modelValue === valueFn(scope, locals);
|
||||
}
|
||||
selectedSet = selectedSet || selected; // see if at least one item is selected
|
||||
}
|
||||
label = displayFn(scope, locals); // what will be seen by the user
|
||||
label = label === undefined ? '' : label; // doing displayFn(scope, locals) || '' overwrites zero values
|
||||
optionGroup.push({
|
||||
id: keyName ? keys[index] : index, // either the index into array or key from object
|
||||
id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), // either the index into array or key from object
|
||||
label: label,
|
||||
selected: selected // determine if we should be selected
|
||||
});
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ describe('select', function() {
|
|||
expect(function() {
|
||||
compile('<select ng-model="selected" ng-options="i dont parse"></select>');
|
||||
}).toThrow("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in" +
|
||||
" _collection_' but got 'i dont parse'.");
|
||||
" _collection_ (track by _expr_)?' but got 'i dont parse'.");
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -753,6 +753,37 @@ 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'
|
||||
});
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.values = [{id: 1, name: 'first'},
|
||||
{id: 2, name: 'second'},
|
||||
{id: 3, name: 'third'},
|
||||
{id: 4, name: 'forth'}];
|
||||
scope.selected = {id: 2};
|
||||
});
|
||||
|
||||
expect(element.val()).toEqual('2');
|
||||
|
||||
var first = jqLite(element.find('option')[0]);
|
||||
expect(first.text()).toEqual('first');
|
||||
expect(first.attr('value')).toEqual('1');
|
||||
var forth = jqLite(element.find('option')[3]);
|
||||
expect(forth.text()).toEqual('forth');
|
||||
expect(forth.attr('value')).toEqual('4');
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.selected = scope.values[3];
|
||||
});
|
||||
|
||||
expect(element.val()).toEqual('4');
|
||||
});
|
||||
|
||||
|
||||
it('should bind to scope value through experession', function() {
|
||||
createSelect({
|
||||
'ng-model': 'selected',
|
||||
|
|
|
|||
Loading…
Reference in a new issue