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

Commit c32a859

Browse files
quazziepetebacondarwin
authored andcommitted
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.
1 parent 4acc28a commit c32a859

File tree

2 files changed

+80
-13
lines changed

2 files changed

+80
-13
lines changed

src/ng/directive/select.js

+48-12
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
* * `label` **`for`** `value` **`in`** `array`
4040
* * `select` **`as`** `label` **`for`** `value` **`in`** `array`
4141
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
42-
* * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
42+
* * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
4343
* * for object data sources:
4444
* * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
4545
* * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
@@ -59,6 +59,9 @@
5959
* element. If not specified, `select` expression will default to `value`.
6060
* * `group`: The result of this expression will be used to group options using the `<optgroup>`
6161
* DOM element.
62+
* * `trackexpr`: Used when working with an array of objects. The result of this expression will be
63+
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
64+
* `value` variable (e.g. `value.propertyName`).
6265
*
6366
* @example
6467
<doc:example>
@@ -123,8 +126,8 @@
123126

124127
var ngOptionsDirective = valueFn({ terminal: true });
125128
var selectDirective = ['$compile', '$parse', function($compile, $parse) {
126-
//0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000077770
127-
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+(.*)$/,
129+
//0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000007777000000000000000000088888
130+
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+(.*?))?$/,
128131
nullModelCtrl = {$setViewValue: noop};
129132

130133
return {
@@ -298,7 +301,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
298301

299302
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
300303
throw Error(
301-
"Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
304+
"Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_ (track by _expr_)?'" +
302305
" but got '" + optionsExp + "'.");
303306
}
304307

@@ -308,6 +311,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
308311
groupByFn = $parse(match[3] || ''),
309312
valueFn = $parse(match[2] ? match[1] : valueName),
310313
valuesFn = $parse(match[7]),
314+
track = match[8],
315+
trackFn = track ? $parse(match[8]) : null,
311316
// This is an array of array of existing option groups in DOM. We try to reuse these if possible
312317
// optionGroupsCache[0] is the options with no option group
313318
// optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
@@ -348,7 +353,14 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
348353
if ((optionElement = optionGroup[index].element)[0].selected) {
349354
key = optionElement.val();
350355
if (keyName) locals[keyName] = key;
351-
locals[valueName] = collection[key];
356+
if (trackFn) {
357+
for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) {
358+
locals[valueName] = collection[trackIndex];
359+
if (trackFn(scope, locals) == key) break;
360+
}
361+
} else {
362+
locals[valueName] = collection[key];
363+
}
352364
value.push(valueFn(scope, locals));
353365
}
354366
}
@@ -360,9 +372,19 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
360372
} else if (key == ''){
361373
value = null;
362374
} else {
363-
locals[valueName] = collection[key];
364-
if (keyName) locals[keyName] = key;
365-
value = valueFn(scope, locals);
375+
if (trackFn) {
376+
for (var trackIndex = 0; trackIndex < collection.length; trackIndex++) {
377+
locals[valueName] = collection[trackIndex];
378+
if (trackFn(scope, locals) == key) {
379+
value = valueFn(scope, locals);
380+
break;
381+
}
382+
}
383+
} else {
384+
locals[valueName] = collection[key];
385+
if (keyName) locals[keyName] = key;
386+
value = valueFn(scope, locals);
387+
}
366388
}
367389
}
368390
ctrl.$setViewValue(value);
@@ -394,7 +416,15 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
394416
label;
395417

396418
if (multiple) {
397-
selectedSet = new HashMap(modelValue);
419+
if (trackFn && isArray(modelValue)) {
420+
selectedSet = new HashMap([]);
421+
for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) {
422+
locals[valueName] = modelValue[trackIndex];
423+
selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]);
424+
}
425+
} else {
426+
selectedSet = new HashMap(modelValue);
427+
}
398428
}
399429

400430
// We now build up the list of options we need (we merge later)
@@ -406,15 +436,21 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
406436
optionGroupNames.push(optionGroupName);
407437
}
408438
if (multiple) {
409-
selected = selectedSet.remove(valueFn(scope, locals)) != undefined;
439+
selected = selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) != undefined;
410440
} else {
411-
selected = modelValue === valueFn(scope, locals);
441+
if (trackFn) {
442+
var modelCast = {};
443+
modelCast[valueName] = modelValue;
444+
selected = trackFn(scope, modelCast) === trackFn(scope, locals);
445+
} else {
446+
selected = modelValue === valueFn(scope, locals);
447+
}
412448
selectedSet = selectedSet || selected; // see if at least one item is selected
413449
}
414450
label = displayFn(scope, locals); // what will be seen by the user
415451
label = label === undefined ? '' : label; // doing displayFn(scope, locals) || '' overwrites zero values
416452
optionGroup.push({
417-
id: keyName ? keys[index] : index, // either the index into array or key from object
453+
id: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), // either the index into array or key from object
418454
label: label,
419455
selected: selected // determine if we should be selected
420456
});

test/ng/directive/selectSpec.js

+32-1
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ describe('select', function() {
495495
expect(function() {
496496
compile('<select ng-model="selected" ng-options="i dont parse"></select>');
497497
}).toThrow("Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in" +
498-
" _collection_' but got 'i dont parse'.");
498+
" _collection_ (track by _expr_)?' but got 'i dont parse'.");
499499
});
500500

501501

@@ -753,6 +753,37 @@ describe('select', function() {
753753
});
754754

755755

756+
it('should bind to scope value and track/identify objects', function() {
757+
createSelect({
758+
'ng-model': 'selected',
759+
'ng-options': 'item as item.name for item in values track by item.id'
760+
});
761+
762+
scope.$apply(function() {
763+
scope.values = [{id: 1, name: 'first'},
764+
{id: 2, name: 'second'},
765+
{id: 3, name: 'third'},
766+
{id: 4, name: 'forth'}];
767+
scope.selected = {id: 2};
768+
});
769+
770+
expect(element.val()).toEqual('2');
771+
772+
var first = jqLite(element.find('option')[0]);
773+
expect(first.text()).toEqual('first');
774+
expect(first.attr('value')).toEqual('1');
775+
var forth = jqLite(element.find('option')[3]);
776+
expect(forth.text()).toEqual('forth');
777+
expect(forth.attr('value')).toEqual('4');
778+
779+
scope.$apply(function() {
780+
scope.selected = scope.values[3];
781+
});
782+
783+
expect(element.val()).toEqual('4');
784+
});
785+
786+
756787
it('should bind to scope value through experession', function() {
757788
createSelect({
758789
'ng-model': 'selected',

0 commit comments

Comments
 (0)