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

Commit 7802c90

Browse files
committed
fix(directive): ng:options to support iterating over objects
Closes #448
1 parent c348f2c commit 7802c90

File tree

3 files changed

+93
-20
lines changed

3 files changed

+93
-20
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
### Bug Fixes
55
- Issue #449: [ng:options] should support binding to a property of an item.
66
- Issue #464: [ng:options] incorrectly re-grew options on datasource change
7+
- Issue #448: [ng:options] should support iterating over objects
78

89
### Breaking changes
910
- no longer support MMMMM in filter.date as we need to follow UNICODE LOCALE DATA formats.

src/widgets.js

+42-19
Original file line numberDiff line numberDiff line change
@@ -596,14 +596,23 @@ angularWidget('button', inputWidgetSelector);
596596
* * binding to a value not in list confuses most browsers.
597597
*
598598
* @element select
599-
* @param {comprehension_expression} comprehension _select_ `as` _label_ `for` _item_ `in` _array_.
599+
* @param {comprehension_expression} comprehension in following form
600600
*
601-
* * _array_: an expression which evaluates to an array of objects to bind.
602-
* * _item_: local variable which will refer to the item in the _array_ during the iteration
601+
* * _select_ `for` _value_ `in` _array_
602+
* * _select_ `as` _label_ `for` _value_ `in` _array_
603+
* * _select_ `for` `(`_key_`,` _value_`)` `in` _object_
604+
* * _select_ `as` _label_ `for` `(`_key_`,` _value_`)` `in` _object_
605+
*
606+
* Where:
607+
*
608+
* * _array_ / _object_: an expression which evaluates to an array / object to iterate over.
609+
* * _value_: local variable which will reffer to the item in the _array_ or _object_ during
610+
* iteration
611+
* * _key_: local variable which will refer to the key in the _object_ during the iteration
603612
* * _select_: The result of this expression will be assigned to the scope.
604613
* The _select_ can be ommited, in which case the _item_ itself will be assigned.
605614
* * _label_: The result of this expression will be the `option` label. The
606-
* `expression` most likely reffers to the _item_ variable. (optional)
615+
* `expression` most likely refers to the _item_ variable. (optional)
607616
*
608617
* @example
609618
<doc:example>
@@ -658,8 +667,8 @@ angularWidget('button', inputWidgetSelector);
658667
</doc:scenario>
659668
</doc:example>
660669
*/
661-
662-
var NG_OPTIONS_REGEXP = /^\s*((.*)\s+as\s+)?(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/;
670+
// 000012222111111111133330000000004555555555555555554666666777777777777777776666666888888888888888888888864000000009999
671+
var NG_OPTIONS_REGEXP = /^\s*((.*)\s+as\s+)?(.*)\s+for\s+(([\$\w][\$\w\d]*)|(\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
663672
angularWidget('select', function(element){
664673
this.descend(true);
665674
this.directives(true);
@@ -671,13 +680,14 @@ angularWidget('select', function(element){
671680
}
672681
if (! (match = expression.match(NG_OPTIONS_REGEXP))) {
673682
throw Error(
674-
"Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got '" +
683+
"Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '" +
675684
expression + "'.");
676685
}
677686
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;
687+
var valueName = match[5] || match[8];
688+
var keyName = match[7];
689+
var valueFn = expressionCompile(match[2] || valueName).fnSelf;
690+
var valuesFn = expressionCompile(match[9]).fnSelf;
681691
// we can't just jqLite('<option>') since jqLite is not smart enough
682692
// to create it in <select> and IE barfs otherwise.
683693
var option = jqLite(document.createElement('option'));
@@ -696,7 +706,7 @@ angularWidget('select', function(element){
696706
});
697707

698708
select.bind('change', function(){
699-
var collection = collectionFn(scope) || [];
709+
var collection = valuesFn(scope) || [];
700710
var value = select.val();
701711
var index, length;
702712
var tempScope = scope.$new();
@@ -705,8 +715,8 @@ angularWidget('select', function(element){
705715
value = [];
706716
for (index = 0, length = optionElements.length; index < length; index++) {
707717
if (optionElements[index][0].selected) {
708-
tempScope[itemName] = collection[index];
709-
value.push(itemFn(tempScope));
718+
tempScope[valueName] = collection[index];
719+
value.push(valueFn(tempScope));
710720
}
711721
}
712722
} else {
@@ -715,8 +725,8 @@ angularWidget('select', function(element){
715725
} else if (value == ''){
716726
value = null;
717727
} else {
718-
tempScope[itemName] = collection[value];
719-
value = itemFn(tempScope);
728+
tempScope[valueName] = collection[value];
729+
value = valueFn(tempScope);
720730
}
721731
}
722732
if (!isUndefined(value)) model.set(value);
@@ -730,7 +740,9 @@ angularWidget('select', function(element){
730740

731741
scope.$onEval(function(){
732742
var scope = this;
733-
var collection = collectionFn(scope) || [];
743+
var values = valuesFn(scope) || [];
744+
var keys = values;
745+
var key;
734746
var value;
735747
var length;
736748
var fragment;
@@ -753,9 +765,20 @@ angularWidget('select', function(element){
753765
}
754766
}
755767

756-
for (index = 0, length = collection.length; index < length; index++) {
757-
optionScope[itemName] = collection[index];
758-
currentItem = itemFn(optionScope);
768+
// If we have a keyName then we are itterating over on object. We
769+
// grab the keys and sort them.
770+
if(keyName) {
771+
keys = [];
772+
for (key in values) {
773+
if (values.hasOwnProperty(key))
774+
keys.push(key);
775+
}
776+
keys.sort();
777+
}
778+
779+
for (index = 0; length = keys.length, index < length; index++) {
780+
optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index];
781+
currentItem = valueFn(optionScope);
759782
optionText = displayFn(optionScope);
760783
if (optionTexts.length > index) {
761784
// reuse

test/widgetsSpec.js

+50-1
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,7 @@ describe("widget", function(){
611611
it('should throw when not formated "? for ? in ?"', function(){
612612
expect(function(){
613613
compile('<select name="selected" ng:options="i dont parse"></select>');
614-
}).toThrow("Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got 'i dont parse'.");
614+
}).toThrow("Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got 'i dont parse'.");
615615

616616
$logMock.error.logs.shift();
617617
});
@@ -628,6 +628,27 @@ describe("widget", function(){
628628
expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>');
629629
});
630630

631+
it('should render an object', function(){
632+
createSelect({
633+
name:'selected',
634+
'ng:options': 'value as key for (key, value) in object'
635+
});
636+
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
637+
scope.selected = scope.object.red;
638+
scope.$eval();
639+
var options = select.find('option');
640+
expect(options.length).toEqual(3);
641+
expect(sortedHtml(options[0])).toEqual('<option value="0">blue</option>');
642+
expect(sortedHtml(options[1])).toEqual('<option value="1">green</option>');
643+
expect(sortedHtml(options[2])).toEqual('<option value="2">red</option>');
644+
expect(options[2].selected).toEqual(true);
645+
646+
scope.object.azur = '8888FF';
647+
scope.$eval();
648+
options = select.find('option');
649+
expect(options[3].selected).toEqual(true);
650+
});
651+
631652
it('should grow list', function(){
632653
createSingleSelect();
633654
scope.values = [];
@@ -751,6 +772,34 @@ describe("widget", function(){
751772
expect(select.val()).toEqual('1');
752773
});
753774

775+
it('should bind to object key', function(){
776+
createSelect({
777+
name:'selected',
778+
'ng:options':'key as value for (key, value) in object'});
779+
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
780+
scope.selected = 'green';
781+
scope.$eval();
782+
expect(select.val()).toEqual('1');
783+
784+
scope.selected = 'blue';
785+
scope.$eval();
786+
expect(select.val()).toEqual('0');
787+
});
788+
789+
it('should bind to object value', function(){
790+
createSelect({
791+
name:'selected',
792+
'ng:options':'value as key for (key, value) in object'});
793+
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
794+
scope.selected = '00FF00';
795+
scope.$eval();
796+
expect(select.val()).toEqual('1');
797+
798+
scope.selected = '0000FF';
799+
scope.$eval();
800+
expect(select.val()).toEqual('0');
801+
});
802+
754803
it('should insert a blank option if bound to null', function(){
755804
createSingleSelect();
756805
scope.values = [{name:'A'}];

0 commit comments

Comments
 (0)