From f3456dc2826e9570cf2969fab3c314255d16188f Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 7 Jul 2011 13:56:13 -0700 Subject: [PATCH] fix(directive): ng:options now support binding to expression Closes #449 --- CHANGELOG.md | 4 +++ src/widgets.js | 76 +++++++++++++++++++++++++++------------------ test/widgetsSpec.js | 53 +++++++++++++++++++++++++------ 3 files changed, 92 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2dcc97ecec..e833ba786cae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 0.9.18 jiggling-armfat (in-progress) # + +### Bug Fixes +- Issue #449: [ng:options] should support binding to a property of an item. + ### 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 478695357445..8fa8db4ad19c 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -596,12 +596,14 @@ angularWidget('button', inputWidgetSelector); * * binding to a value not in list confuses most browsers. * * @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. * * _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 - * `expression` most likely refers to the _item_ variable. + * * _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. + * * _label_: The result of this expression will be the `option` label. The + * `expression` most likely reffers to the _item_ variable. (optional) * * @example @@ -657,7 +659,7 @@ angularWidget('button', inputWidgetSelector); */ -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){ this.descend(true); this.directives(true); @@ -669,12 +671,13 @@ angularWidget('select', function(element){ } if (! (match = expression.match(NG_OPTIONS_REGEXP))) { 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 + "'."); } - var displayFn = expressionCompile(match[1]).fnSelf; - var itemName = match[2]; - var collectionFn = expressionCompile(match[3]).fnSelf; + var displayFn = expressionCompile(match[3]).fnSelf; + var itemName = match[4]; + var itemFn = expressionCompile(match[2] || itemName).fnSelf; + var collectionFn = expressionCompile(match[5]).fnSelf; // we can't just jqLite('' : '') + - (unknown ? '' : '') + - ''); + function createSelect(attrs, blank, unknown){ + var html = 'blank' : '') + + (unknown ? '' : '') + + ''; + select = jqLite(html); scope = compile(select); }; 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){ - createSelect(true, blank, unknown); + createSelect({name:'selected', multiple:true, 'ng:options':'value.name for value in values'}, + blank, unknown); }; afterEach(function(){ @@ -602,7 +611,7 @@ describe("widget", function(){ it('should throw when not formated "? for ? in ?"', function(){ expect(function(){ compile(''); - }).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(); }); @@ -712,6 +721,18 @@ describe("widget", function(){ 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(){ createSingleSelect(); scope.values = [{name:'A'}]; @@ -771,6 +792,18 @@ describe("widget", function(){ 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(){ createSingleSelect(true); scope.values = [{name:'A'}, {name:'B'}];