diff options
| author | Misko Hevery | 2011-07-14 13:27:39 -0700 | 
|---|---|---|
| committer | Misko Hevery | 2011-07-26 09:41:43 -0700 | 
| commit | 7802c90e139a36d37b9d3c8cd6b6fcfee042dd71 (patch) | |
| tree | 3dcdea5cfe93a80bc0d5dee68d45c87b4f7f8908 | |
| parent | c348f2cad6e2db0c3a37108eb34c8d62f2b7c718 (diff) | |
| download | angular.js-7802c90e139a36d37b9d3c8cd6b6fcfee042dd71.tar.bz2 | |
fix(directive): ng:options to support iterating over objects
Closes #448
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | src/widgets.js | 61 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 51 | 
3 files changed, 93 insertions, 20 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ac5c63..c3b9251b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@  ### Bug Fixes  - Issue #449: [ng:options] should support binding to a property of an item.  - Issue #464: [ng:options] incorrectly re-grew options on datasource change +- Issue #448: [ng:options] should support iterating over objects  ### 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 f8ada667..bdd68804 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -596,14 +596,23 @@ angularWidget('button', inputWidgetSelector);   *   * binding to a value not in list confuses most browsers.   *   * @element select - * @param {comprehension_expression} comprehension _select_ `as` _label_ `for` _item_ `in` _array_. + * @param {comprehension_expression} comprehension in following form   * - *   * _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 + *   * _select_  `for` _value_ `in` _array_ + *   * _select_ `as` _label_ `for` _value_ `in` _array_ + *   * _select_  `for` `(`_key_`,` _value_`)` `in` _object_ + *   * _select_ `as` _label_ `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.   *   * _label_: The result of this expression will be the `option` label. The - *        `expression` most likely reffers to the _item_ variable. (optional) + *        `expression` most likely refers to the _item_ variable. (optional)   *   * @example      <doc:example> @@ -658,8 +667,8 @@ angularWidget('button', inputWidgetSelector);        </doc:scenario>      </doc:example>   */ - -var NG_OPTIONS_REGEXP = /^\s*((.*)\s+as\s+)?(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/; +//                       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+(.*)$/;  angularWidget('select', function(element){    this.descend(true);    this.directives(true); @@ -671,13 +680,14 @@ angularWidget('select', function(element){    }    if (! (match = expression.match(NG_OPTIONS_REGEXP))) {      throw Error( -        "Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got '" + +        "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '" +          expression + "'.");    }    var displayFn = expressionCompile(match[3]).fnSelf; -  var itemName = match[4]; -  var itemFn = expressionCompile(match[2] || itemName).fnSelf; -  var collectionFn = expressionCompile(match[5]).fnSelf; +  var valueName = match[5] || match[8]; +  var keyName = match[7]; +  var valueFn = expressionCompile(match[2] || valueName).fnSelf; +  var valuesFn = expressionCompile(match[9]).fnSelf;    // we can't just jqLite('<option>') since jqLite is not smart enough    // to create it in <select> and IE barfs otherwise.    var option = jqLite(document.createElement('option')); @@ -696,7 +706,7 @@ angularWidget('select', function(element){      });      select.bind('change', function(){ -      var collection = collectionFn(scope) || []; +      var collection = valuesFn(scope) || [];        var value = select.val();        var index, length;        var tempScope = scope.$new(); @@ -705,8 +715,8 @@ angularWidget('select', function(element){            value = [];            for (index = 0, length = optionElements.length; index < length; index++) {              if (optionElements[index][0].selected) { -              tempScope[itemName] = collection[index]; -              value.push(itemFn(tempScope)); +              tempScope[valueName] = collection[index]; +              value.push(valueFn(tempScope));              }            }          } else { @@ -715,8 +725,8 @@ angularWidget('select', function(element){            } else if (value == ''){              value = null;            } else { -            tempScope[itemName] = collection[value]; -            value = itemFn(tempScope); +            tempScope[valueName] = collection[value]; +            value = valueFn(tempScope);            }          }          if (!isUndefined(value)) model.set(value); @@ -730,7 +740,9 @@ angularWidget('select', function(element){      scope.$onEval(function(){        var scope = this; -      var collection = collectionFn(scope) || []; +      var values = valuesFn(scope) || []; +      var keys = values; +      var key;        var value;        var length;        var fragment; @@ -753,9 +765,20 @@ angularWidget('select', function(element){            }          } -        for (index = 0, length = collection.length; index < length; index++) { -          optionScope[itemName] = collection[index]; -          currentItem = itemFn(optionScope); +        // If we have a keyName then we are itterating over on object. We +        // grab the keys and sort them. +        if(keyName) { +          keys = []; +          for (key in values) { +            if (values.hasOwnProperty(key)) +              keys.push(key); +          } +          keys.sort(); +        } + +        for (index = 0; length = keys.length, index < length; index++) { +          optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index]; +          currentItem = valueFn(optionScope);            optionText = displayFn(optionScope);            if (optionTexts.length > index) {              // reuse diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 342dc8c5..422ca86b 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -611,7 +611,7 @@ describe("widget", function(){      it('should throw when not formated "? for ? in ?"', function(){        expect(function(){          compile('<select name="selected" ng:options="i dont parse"></select>'); -      }).toThrow("Expected ng:options in form of '(_expression_ as)? _expresion_ for _item_ in _collection_' but got 'i dont parse'."); +      }).toThrow("Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got 'i dont parse'.");        $logMock.error.logs.shift();      }); @@ -628,6 +628,27 @@ describe("widget", function(){        expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>');      }); +    it('should render an object', function(){ +      createSelect({ +        name:'selected', +        'ng:options': 'value as key for (key, value) in object' +      }); +      scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; +      scope.selected = scope.object.red; +      scope.$eval(); +      var options = select.find('option'); +      expect(options.length).toEqual(3); +      expect(sortedHtml(options[0])).toEqual('<option value="0">blue</option>'); +      expect(sortedHtml(options[1])).toEqual('<option value="1">green</option>'); +      expect(sortedHtml(options[2])).toEqual('<option value="2">red</option>'); +      expect(options[2].selected).toEqual(true); + +      scope.object.azur = '8888FF'; +      scope.$eval(); +      options = select.find('option'); +      expect(options[3].selected).toEqual(true); +    }); +      it('should grow list', function(){        createSingleSelect();        scope.values = []; @@ -751,6 +772,34 @@ describe("widget", function(){          expect(select.val()).toEqual('1');        }); +      it('should bind to object key', function(){ +        createSelect({ +          name:'selected', +          'ng:options':'key as value for (key, value) in object'}); +        scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; +        scope.selected = 'green'; +        scope.$eval(); +        expect(select.val()).toEqual('1'); + +        scope.selected = 'blue'; +        scope.$eval(); +        expect(select.val()).toEqual('0'); +      }); + +      it('should bind to object value', function(){ +        createSelect({ +          name:'selected', +          'ng:options':'value as key for (key, value) in object'}); +        scope.object = {'red':'FF0000', 'green':'00FF00', 'blue':'0000FF'}; +        scope.selected = '00FF00'; +        scope.$eval(); +        expect(select.val()).toEqual('1'); + +        scope.selected = '0000FF'; +        scope.$eval(); +        expect(select.val()).toEqual('0'); +      }); +        it('should insert a blank option if bound to null', function(){          createSingleSelect();          scope.values = [{name:'A'}]; | 
