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

Commit f3456dc

Browse files
committed
fix(directive): ng:options now support binding to expression
Closes #449
1 parent ee04141 commit f3456dc

File tree

3 files changed

+92
-41
lines changed

3 files changed

+92
-41
lines changed

CHANGELOG.md

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

src/widgets.js

+45-31
Original file line numberDiff line numberDiff line change
@@ -596,12 +596,14 @@ angularWidget('button', inputWidgetSelector);
596596
* * binding to a value not in list confuses most browsers.
597597
*
598598
* @element select
599-
* @param {comprehension_expression} comprehension _expresion_ `for` _item_ `in` _array_.
599+
* @param {comprehension_expression} comprehension _select_ `as` _label_ `for` _item_ `in` _array_.
600600
*
601601
* * _array_: an expression which evaluates to an array of objects to bind.
602602
* * _item_: local variable which will refer to the item in the _array_ during the iteration
603-
* * _expression_: The result of this expression will be `option` label. The
604-
* `expression` most likely refers to the _item_ variable.
603+
* * _select_: The result of this expression will be assigned to the scope.
604+
* The _select_ can be ommited, in which case the _item_ itself will be assigned.
605+
* * _label_: The result of this expression will be the `option` label. The
606+
* `expression` most likely reffers to the _item_ variable. (optional)
605607
*
606608
* @example
607609
<doc:example>
@@ -657,7 +659,7 @@ angularWidget('button', inputWidgetSelector);
657659
</doc:example>
658660
*/
659661

660-
var NG_OPTIONS_REGEXP = /^(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/;
662+
var NG_OPTIONS_REGEXP = /^\s*((.*)\s+as\s+)?(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/;
661663
angularWidget('select', function(element){
662664
this.descend(true);
663665
this.directives(true);
@@ -669,12 +671,13 @@ angularWidget('select', function(element){
669671
}
670672
if (! (match = expression.match(NG_OPTIONS_REGEXP))) {
671673
throw Error(
672-
"Expected ng:options in form of '_expresion_ for _item_ in _collection_' but got '" +
674+
"Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got '" +
673675
expression + "'.");
674676
}
675-
var displayFn = expressionCompile(match[1]).fnSelf;
676-
var itemName = match[2];
677-
var collectionFn = expressionCompile(match[3]).fnSelf;
677+
var displayFn = expressionCompile(match[3]).fnSelf;
678+
var itemName = match[4];
679+
var itemFn = expressionCompile(match[2] || itemName).fnSelf;
680+
var collectionFn = expressionCompile(match[5]).fnSelf;
678681
// we can't just jqLite('<option>') since jqLite is not smart enough
679682
// to create it in <select> and IE barfs otherwise.
680683
var option = jqLite(document.createElement('option'));
@@ -696,24 +699,33 @@ angularWidget('select', function(element){
696699
var collection = collectionFn(scope) || [];
697700
var value = select.val();
698701
var index, length;
699-
if (isMultiselect) {
700-
value = [];
701-
for (index = 0, length = optionElements.length; index < length; index++) {
702-
if (optionElements[index][0].selected) {
703-
value.push(collection[index]);
702+
var tempScope = scope.$new();
703+
try {
704+
if (isMultiselect) {
705+
value = [];
706+
for (index = 0, length = optionElements.length; index < length; index++) {
707+
if (optionElements[index][0].selected) {
708+
tempScope[itemName] = collection[index];
709+
value.push(itemFn(tempScope));
710+
}
704711
}
705-
}
706-
} else {
707-
if (value == '?') {
708-
value = undefined;
709712
} else {
710-
value = (value == '' ? null : collection[value]);
713+
if (value == '?') {
714+
value = undefined;
715+
} else if (value == ''){
716+
value = null;
717+
} else {
718+
tempScope[itemName] = collection[value];
719+
value = itemFn(tempScope);
720+
}
711721
}
722+
if (!isUndefined(value)) model.set(value);
723+
scope.$tryEval(function(){
724+
scope.$root.$eval();
725+
});
726+
} finally {
727+
tempScope = null; // TODO(misko): needs to be $destroy
712728
}
713-
if (!isUndefined(value)) model.set(value);
714-
scope.$tryEval(function(){
715-
scope.$root.$eval();
716-
});
717729
});
718730

719731
scope.$onEval(function(){
@@ -731,17 +743,19 @@ angularWidget('select', function(element){
731743
var selectValue = '';
732744
var isMulti = isMultiselect;
733745

734-
if (isMulti) {
735-
selectValue = new HashMap();
736-
if (modelValue && isNumber(length = modelValue.length)) {
737-
for (index = 0; index < length; index++) {
738-
selectValue.put(modelValue[index], true);
746+
try {
747+
if (isMulti) {
748+
selectValue = new HashMap();
749+
if (modelValue && isNumber(length = modelValue.length)) {
750+
for (index = 0; index < length; index++) {
751+
selectValue.put(modelValue[index], true);
752+
}
739753
}
740754
}
741-
}
742-
try {
755+
743756
for (index = 0, length = collection.length; index < length; index++) {
744-
currentItem = optionScope[itemName] = collection[index];
757+
optionScope[itemName] = collection[index];
758+
currentItem = itemFn(optionScope);
745759
optionText = displayFn(optionScope);
746760
if (optionTexts.length > index) {
747761
// reuse
@@ -799,7 +813,7 @@ angularWidget('select', function(element){
799813
}
800814

801815
} finally {
802-
optionScope = null;
816+
optionScope = null; // TODO(misko): needs to be $destroy()
803817
}
804818
});
805819
};

test/widgetsSpec.js

+43-10
Original file line numberDiff line numberDiff line change
@@ -576,22 +576,31 @@ describe("widget", function(){
576576
describe('ng:options', function(){
577577
var select, scope;
578578

579-
function createSelect(multiple, blank, unknown){
580-
select = jqLite(
581-
'<select name="selected" ' + (multiple ? ' multiple' : '') +
582-
' ng:options="value.name for value in values">' +
583-
(blank ? '<option value="">blank</option>' : '') +
584-
(unknown ? '<option value="?">unknown</option>' : '') +
585-
'</select>');
579+
function createSelect(attrs, blank, unknown){
580+
var html = '<select';
581+
forEach(attrs, function(value, key){
582+
if (typeof value == 'boolean') {
583+
if (value) html += ' ' + key;
584+
} else {
585+
html+= ' ' + key + '="' + value + '"';
586+
}
587+
});
588+
html += '>' +
589+
(blank ? '<option value="">blank</option>' : '') +
590+
(unknown ? '<option value="?">unknown</option>' : '') +
591+
'</select>';
592+
select = jqLite(html);
586593
scope = compile(select);
587594
};
588595

589596
function createSingleSelect(blank, unknown){
590-
createSelect(false, blank, unknown);
597+
createSelect({name:'selected', 'ng:options':'value.name for value in values'},
598+
blank, unknown);
591599
};
592600

593601
function createMultiSelect(blank, unknown){
594-
createSelect(true, blank, unknown);
602+
createSelect({name:'selected', multiple:true, 'ng:options':'value.name for value in values'},
603+
blank, unknown);
595604
};
596605

597606
afterEach(function(){
@@ -602,7 +611,7 @@ describe("widget", function(){
602611
it('should throw when not formated "? for ? in ?"', function(){
603612
expect(function(){
604613
compile('<select name="selected" ng:options="i dont parse"></select>');
605-
}).toThrow("Expected ng:options in form of '_expresion_ for _item_ in _collection_' but got 'i dont parse'.");
614+
}).toThrow("Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got 'i dont parse'.");
606615

607616
$logMock.error.logs.shift();
608617
});
@@ -712,6 +721,18 @@ describe("widget", function(){
712721
expect(select.val()).toEqual('1');
713722
});
714723

724+
it('should bind to scope value through experession', function(){
725+
createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'});
726+
scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
727+
scope.selected = scope.values[0].id;
728+
scope.$eval();
729+
expect(select.val()).toEqual('0');
730+
731+
scope.selected = scope.values[1].id;
732+
scope.$eval();
733+
expect(select.val()).toEqual('1');
734+
});
735+
715736
it('should insert a blank option if bound to null', function(){
716737
createSingleSelect();
717738
scope.values = [{name:'A'}];
@@ -771,6 +792,18 @@ describe("widget", function(){
771792
expect(scope.selected).toEqual(scope.values[1]);
772793
});
773794

795+
it('should update model on change through expression', function(){
796+
createSelect({name:'selected', 'ng:options':'item.id as item.name for item in values'});
797+
scope.values = [{id:10, name:'A'}, {id:20, name:'B'}];
798+
scope.selected = scope.values[0].id;
799+
scope.$eval();
800+
expect(select.val()).toEqual('0');
801+
802+
select.val('1');
803+
browserTrigger(select, 'change');
804+
expect(scope.selected).toEqual(scope.values[1].id);
805+
});
806+
774807
it('should update model to null on change', function(){
775808
createSingleSelect(true);
776809
scope.values = [{name:'A'}, {name:'B'}];

0 commit comments

Comments
 (0)