mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-05-25 14:23:43 +00:00
fix(directive): ng:options now support binding to expression
Closes #449
This commit is contained in:
parent
ee04141a5a
commit
f3456dc282
3 changed files with 93 additions and 42 deletions
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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'}];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue