fix(directive): ng:options now support binding to expression

Closes #449
This commit is contained in:
Misko Hevery 2011-07-07 13:56:13 -07:00
parent ee04141a5a
commit f3456dc282
3 changed files with 93 additions and 42 deletions

View file

@ -1,5 +1,9 @@
<a name="0.9.18"><a/> <a name="0.9.18"><a/>
# <angular/> 0.9.18 jiggling-armfat (in-progress) # # <angular/> 0.9.18 jiggling-armfat (in-progress) #
### Bug Fixes
- Issue #449: [ng:options] should support binding to a property of an item.
### Breaking changes ### Breaking changes
- no longer support MMMMM in filter.date as we need to follow UNICODE LOCALE DATA formats. - no longer support MMMMM in filter.date as we need to follow UNICODE LOCALE DATA formats.

View file

@ -596,12 +596,14 @@ angularWidget('button', inputWidgetSelector);
* * binding to a value not in list confuses most browsers. * * binding to a value not in list confuses most browsers.
* *
* @element select * @element select
* @param {comprehension_expression} comprehension _expresion_ `for` _item_ `in` _array_. * @param {comprehension_expression} comprehension _select_ `as` _label_ `for` _item_ `in` _array_.
* *
* * _array_: an expression which evaluates to an array of objects to bind. * * _array_: an expression which evaluates to an array of objects to bind.
* * _item_: local variable which will refer to the item in the _array_ during the iteration * * _item_: local variable which will refer to the item in the _array_ during the iteration
* * _expression_: The result of this expression will be `option` label. The * * _select_: The result of this expression will be assigned to the scope.
* `expression` most likely refers to the _item_ variable. * The _select_ can be ommited, in which case the _item_ itself will be assigned.
* * _label_: The result of this expression will be the `option` label. The
* `expression` most likely reffers to the _item_ variable. (optional)
* *
* @example * @example
<doc:example> <doc:example>
@ -657,7 +659,7 @@ angularWidget('button', inputWidgetSelector);
</doc:example> </doc:example>
*/ */
var NG_OPTIONS_REGEXP = /^(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/; var NG_OPTIONS_REGEXP = /^\s*((.*)\s+as\s+)?(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/;
angularWidget('select', function(element){ angularWidget('select', function(element){
this.descend(true); this.descend(true);
this.directives(true); this.directives(true);
@ -669,12 +671,13 @@ angularWidget('select', function(element){
} }
if (! (match = expression.match(NG_OPTIONS_REGEXP))) { if (! (match = expression.match(NG_OPTIONS_REGEXP))) {
throw Error( throw Error(
"Expected ng:options in form of '_expresion_ for _item_ in _collection_' but got '" + "Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got '" +
expression + "'."); expression + "'.");
} }
var displayFn = expressionCompile(match[1]).fnSelf; var displayFn = expressionCompile(match[3]).fnSelf;
var itemName = match[2]; var itemName = match[4];
var collectionFn = expressionCompile(match[3]).fnSelf; var itemFn = expressionCompile(match[2] || itemName).fnSelf;
var collectionFn = expressionCompile(match[5]).fnSelf;
// we can't just jqLite('<option>') since jqLite is not smart enough // we can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise. // to create it in <select> and IE barfs otherwise.
var option = jqLite(document.createElement('option')); var option = jqLite(document.createElement('option'));
@ -696,24 +699,33 @@ angularWidget('select', function(element){
var collection = collectionFn(scope) || []; var collection = collectionFn(scope) || [];
var value = select.val(); var value = select.val();
var index, length; var index, length;
if (isMultiselect) { var tempScope = scope.$new();
value = []; try {
for (index = 0, length = optionElements.length; index < length; index++) { if (isMultiselect) {
if (optionElements[index][0].selected) { value = [];
value.push(collection[index]); for (index = 0, length = optionElements.length; index < length; index++) {
if (optionElements[index][0].selected) {
tempScope[itemName] = collection[index];
value.push(itemFn(tempScope));
}
}
} else {
if (value == '?') {
value = undefined;
} else if (value == ''){
value = null;
} else {
tempScope[itemName] = collection[value];
value = itemFn(tempScope);
} }
} }
} else { if (!isUndefined(value)) model.set(value);
if (value == '?') { scope.$tryEval(function(){
value = undefined; scope.$root.$eval();
} else { });
value = (value == '' ? null : collection[value]); } finally {
} tempScope = null; // TODO(misko): needs to be $destroy
} }
if (!isUndefined(value)) model.set(value);
scope.$tryEval(function(){
scope.$root.$eval();
});
}); });
scope.$onEval(function(){ scope.$onEval(function(){
@ -731,17 +743,19 @@ angularWidget('select', function(element){
var selectValue = ''; var selectValue = '';
var isMulti = isMultiselect; var isMulti = isMultiselect;
if (isMulti) { try {
selectValue = new HashMap(); if (isMulti) {
if (modelValue && isNumber(length = modelValue.length)) { selectValue = new HashMap();
for (index = 0; index < length; index++) { if (modelValue && isNumber(length = modelValue.length)) {
selectValue.put(modelValue[index], true); for (index = 0; index < length; index++) {
selectValue.put(modelValue[index], true);
}
} }
} }
}
try {
for (index = 0, length = collection.length; index < length; index++) { for (index = 0, length = collection.length; index < length; index++) {
currentItem = optionScope[itemName] = collection[index]; optionScope[itemName] = collection[index];
currentItem = itemFn(optionScope);
optionText = displayFn(optionScope); optionText = displayFn(optionScope);
if (optionTexts.length > index) { if (optionTexts.length > index) {
// reuse // reuse
@ -799,7 +813,7 @@ angularWidget('select', function(element){
} }
} finally { } finally {
optionScope = null; optionScope = null; // TODO(misko): needs to be $destroy()
} }
}); });
}; };

View file

@ -576,22 +576,31 @@ describe("widget", function(){
describe('ng:options', function(){ describe('ng:options', function(){
var select, scope; var select, scope;
function createSelect(multiple, blank, unknown){ function createSelect(attrs, blank, unknown){
select = jqLite( var html = '<select';
'<select name="selected" ' + (multiple ? ' multiple' : '') + forEach(attrs, function(value, key){
' ng:options="value.name for value in values">' + if (typeof value == 'boolean') {
(blank ? '<option value="">blank</option>' : '') + if (value) html += ' ' + key;
(unknown ? '<option value="?">unknown</option>' : '') + } else {
'</select>'); html+= ' ' + key + '="' + value + '"';
}
});
html += '>' +
(blank ? '<option value="">blank</option>' : '') +
(unknown ? '<option value="?">unknown</option>' : '') +
'</select>';
select = jqLite(html);
scope = compile(select); scope = compile(select);
}; };
function createSingleSelect(blank, unknown){ function createSingleSelect(blank, unknown){
createSelect(false, blank, unknown); createSelect({name:'selected', 'ng:options':'value.name for value in values'},
blank, unknown);
}; };
function createMultiSelect(blank, unknown){ function createMultiSelect(blank, unknown){
createSelect(true, blank, unknown); createSelect({name:'selected', multiple:true, 'ng:options':'value.name for value in values'},
blank, unknown);
}; };
afterEach(function(){ afterEach(function(){
@ -602,7 +611,7 @@ describe("widget", function(){
it('should throw when not formated "? for ? in ?"', function(){ it('should throw when not formated "? for ? in ?"', function(){
expect(function(){ expect(function(){
compile('<select name="selected" ng:options="i dont parse"></select>'); compile('<select name="selected" ng:options="i dont parse"></select>');
}).toThrow("Expected ng:options in form of '_expresion_ for _item_ in _collection_' but got 'i dont parse'."); }).toThrow("Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got 'i dont parse'.");
$logMock.error.logs.shift(); $logMock.error.logs.shift();
}); });
@ -712,6 +721,18 @@ describe("widget", function(){
expect(select.val()).toEqual('1'); expect(select.val()).toEqual('1');
}); });
it('should bind to scope value through experession', function(){
createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'});
scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
scope.selected = scope.values[0].id;
scope.$eval();
expect(select.val()).toEqual('0');
scope.selected = scope.values[1].id;
scope.$eval();
expect(select.val()).toEqual('1');
});
it('should insert a blank option if bound to null', function(){ it('should insert a blank option if bound to null', function(){
createSingleSelect(); createSingleSelect();
scope.values = [{name:'A'}]; scope.values = [{name:'A'}];
@ -771,6 +792,18 @@ describe("widget", function(){
expect(scope.selected).toEqual(scope.values[1]); expect(scope.selected).toEqual(scope.values[1]);
}); });
it('should update model on change through expression', function(){
createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'});
scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
scope.selected = scope.values[0].id;
scope.$eval();
expect(select.val()).toEqual('0');
select.val('1');
browserTrigger(select, 'change');
expect(scope.selected).toEqual(scope.values[1].id);
});
it('should update model to null on change', function(){ it('should update model to null on change', function(){
createSingleSelect(true); createSingleSelect(true);
scope.values = [{name:'A'}, {name:'B'}]; scope.values = [{name:'A'}, {name:'B'}];