From f768954f38a7077abfd291eaafc0500d2d1e8007 Mon Sep 17 00:00:00 2001
From: Misko Hevery
Date: Fri, 15 Jul 2011 16:17:05 -0700
Subject: fix(ng:options): add support for option groups
Closes# 450
---
CHANGELOG.md | 1 +
src/widgets.js | 263 ++++++++++++++++++++++++++++++++++------------------
test/widgetsSpec.js | 50 ++++++++--
3 files changed, 214 insertions(+), 100 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2ef1b35d..311b5ed9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
- Issue #464: [ng:options] incorrectly re-grew options on datasource change
- Issue #448: [ng:options] should support iterating over objects
- Issue #463: [ng:options] should support firing ng:change event
+- Issue #450: [ng:options] should support group by (select option groups)
### Breaking changes
- no longer support MMMMM in filter.date as we need to follow UNICODE LOCALE DATA formats.
diff --git a/src/widgets.js b/src/widgets.js
index 17a14741..17059bca 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -598,21 +598,27 @@ angularWidget('button', inputWidgetSelector);
* @element select
* @param {comprehension_expression} comprehension in following form
*
- * * _select_ `for` _value_ `in` _array_
+ * * _label_ `for` _value_ `in` _array_
* * _select_ `as` _label_ `for` _value_ `in` _array_
- * * _select_ `for` `(`_key_`,` _value_`)` `in` _object_
+ * * _select_ `as` _label_ `group by` _group_ `for` _value_ `in` _array_
+ * * _select_ `group by` _group_ `for` _value_ `in` _array_
+ * * _label_ `for` `(`_key_`,` _value_`)` `in` _object_
* * _select_ `as` _label_ `for` `(`_key_`,` _value_`)` `in` _object_
+ * * _select_ `as` _label_ `group by` _group_ `for` `(`_key_`,` _value_`)` `in` _object_
+ * * _select_ `group by` _group_ `for` `(`_key_`,` _value_`)` `in` _object_
*
* Where:
*
* * _array_ / _object_: an expression which evaluates to an array / object to iterate over.
- * * _value_: local variable which will reffer to the item in the _array_ or _object_ during
- * iteration
- * * _key_: local variable which will refer to the key in the _object_ during the iteration
- * * _select_: The result of this expression will be assigned to the scope.
- * The _select_ can be ommited, in which case the _item_ itself will be assigned.
+ * * _value_: local variable which will refer to each item in the _array_ or each value of
+ * _object_ during itteration.
+ * * _key_: local variable which will refer to the key in the _object_ during the iteration.
* * _label_: The result of this expression will be the `option` label. The
- * `expression` most likely refers to the _item_ variable. (optional)
+ * `expression` will most likely refer to the _value_ variable.
+ * * _select_: The result of this expression will be bound to the scope. If not specified,
+ * _select_ expression will default to _value_.
+ * * _group_: The result of this expression will be used to group options using the `optgroup`
+ * DOM element.
*
* @example
@@ -667,8 +673,8 @@ angularWidget('button', inputWidgetSelector);
*/
-// 000012222111111111133330000000004555555555555555554666666777777777777777776666666888888888888888888888864000000009999
-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+(.*)$/;
+// 00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
+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+(.*)$/;
angularWidget('select', function(element){
this.descend(true);
this.directives(true);
@@ -684,53 +690,71 @@ angularWidget('select', function(element){
"Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '" +
expression + "'.");
}
- var displayFn = expressionCompile(match[3]).fnSelf;
- var valueName = match[5] || match[8];
- var keyName = match[7];
- var valueFn = expressionCompile(match[2] || valueName).fnSelf;
- var valuesFn = expressionCompile(match[9]).fnSelf;
+ var displayFn = expressionCompile(match[2] || match[1]).fnSelf;
+ var valueName = match[4] || match[6];
+ var keyName = match[5];
+ var groupByFn = expressionCompile(match[3] || '').fnSelf;
+ var valueFn = expressionCompile(match[2] ? match[1] : valueName).fnSelf;
+ var valuesFn = expressionCompile(match[7]).fnSelf;
// we can't just jqLite('');
- expect(sortedHtml(options[1])).toEqual('');
- expect(sortedHtml(options[2])).toEqual('');
+ expect(sortedHtml(options[0])).toEqual('');
+ expect(sortedHtml(options[1])).toEqual('');
+ expect(sortedHtml(options[2])).toEqual('');
expect(options[2].selected).toEqual(true);
scope.object.azur = '8888FF';
@@ -654,7 +654,7 @@ describe("widget", function(){
scope.values = [];
scope.$eval();
expect(select.find('option').length).toEqual(1); // because we add special empty option
- expect(sortedHtml(select.find('option')[0])).toEqual('');
+ expect(sortedHtml(select.find('option')[0])).toEqual('');
scope.values.push({name:'A'});
scope.selected = scope.values[0];
@@ -760,6 +760,38 @@ describe("widget", function(){
expect(select.val()).toEqual('1');
});
+ it('should bind to scope value and group', function(){
+ createSelect({
+ name:'selected',
+ 'ng:options':'item.name group by item.group for item in values'});
+ scope.values = [{name:'A'},
+ {name:'B', group:'first'},
+ {name:'C', group:'second'},
+ {name:'D', group:'first'},
+ {name:'E', group:'second'}];
+ scope.selected = scope.values[3];
+ scope.$eval();
+ expect(select.val()).toEqual('3');
+
+ var first = jqLite(select.find('optgroup')[0]);
+ var b = jqLite(first.find('option')[0]);
+ var d = jqLite(first.find('option')[1]);
+ expect(first.attr('label')).toEqual('first');
+ expect(b.text()).toEqual('B');
+ expect(d.text()).toEqual('D');
+
+ var second = jqLite(select.find('optgroup')[1]);
+ var c = jqLite(second.find('option')[0]);
+ var e = jqLite(second.find('option')[1]);
+ expect(second.attr('label')).toEqual('second');
+ expect(c.text()).toEqual('C');
+ expect(e.text()).toEqual('E');
+
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.val()).toEqual('0');
+ });
+
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'}];
@@ -779,11 +811,11 @@ describe("widget", function(){
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
scope.selected = 'green';
scope.$eval();
- expect(select.val()).toEqual('1');
+ expect(select.val()).toEqual('green');
scope.selected = 'blue';
scope.$eval();
- expect(select.val()).toEqual('0');
+ expect(select.val()).toEqual('blue');
});
it('should bind to object value', function(){
@@ -793,11 +825,11 @@ describe("widget", function(){
scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'};
scope.selected = '00FF00';
scope.$eval();
- expect(select.val()).toEqual('1');
+ expect(select.val()).toEqual('green');
scope.selected = '0000FF';
scope.$eval();
- expect(select.val()).toEqual('0');
+ expect(select.val()).toEqual('blue');
});
it('should insert a blank option if bound to null', function(){
--
cgit v1.2.3