diff options
| author | Misko Hevery | 2011-01-13 10:35:26 -0800 | 
|---|---|---|
| committer | Misko Hevery | 2011-01-14 10:30:00 -0800 | 
| commit | 347be5ae9aa6829427e1e8e1b1e58afdf2a36c0a (patch) | |
| tree | 3b350a12378c1ec63f60cce0fe674186d204726e | |
| parent | 934f44f69e94a77a3ea6c19dc5c6f82ade2cc669 (diff) | |
| download | angular.js-347be5ae9aa6829427e1e8e1b1e58afdf2a36c0a.tar.bz2 | |
fixed select with ng:format
select (one/multiple) could not chose from a list of objects, since DOM requires string ids.
Solved by adding index formatter, which exposed incorrect handling of formatters in select
widgets.
| -rw-r--r-- | CHANGELOG.md | 10 | ||||
| -rw-r--r-- | docs/spec/ngdocSpec.js | 63 | ||||
| -rw-r--r-- | docs/src/ngdoc.js | 110 | ||||
| -rw-r--r-- | src/JSON.js | 2 | ||||
| -rw-r--r-- | src/directives.js | 8 | ||||
| -rw-r--r-- | src/formatters.js | 62 | ||||
| -rw-r--r-- | src/parser.js | 31 | ||||
| -rw-r--r-- | src/widgets.js | 148 | ||||
| -rw-r--r-- | test/FormattersSpec.js | 15 | ||||
| -rw-r--r-- | test/JsonSpec.js | 17 | ||||
| -rw-r--r-- | test/ParserSpec.js | 25 | ||||
| -rw-r--r-- | test/directivesSpec.js | 15 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 98 | 
13 files changed, 433 insertions, 171 deletions
| diff --git a/CHANGELOG.md b/CHANGELOG.md index eda438e6..9600206b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@  # <angular/> 0.9.10 flea-whisperer  (in-progress) # +### Bug Fixes +- html select (one/multiple) could not chose from a list of objects, since DOM requires string ids.  # <angular/> 0.9.9 time-shift (2011-01-13) # @@ -99,9 +101,9 @@  - small docs improvements (mainly docs for the $resource service)  ### Breaking changes -- Angular expressions in the view used to support regular expressions. This feature was rarely  -  used and added unnecessary complexity. It not a good idea to have regexps in the view anyway,  -  so we removed this support. If you had any regexp in your views, you will have to move them to  +- Angular expressions in the view used to support regular expressions. This feature was rarely +  used and added unnecessary complexity. It not a good idea to have regexps in the view anyway, +  so we removed this support. If you had any regexp in your views, you will have to move them to    your controllers. (commit e5e69d9b90850eb653883f52c76e28dd870ee067) @@ -120,7 +122,7 @@  - docs app UI polishing with dual scrolling and other improvements  ### Bug Fixes -- `select` widget now behaves correctly when it's `option` items are created via `ng:repeat`  +- `select` widget now behaves correctly when it's `option` items are created via `ng:repeat`    (issue #170)  - fix for async xhr cache issue #152 by adding `$browser.defer` and `$defer` service diff --git a/docs/spec/ngdocSpec.js b/docs/spec/ngdocSpec.js index 63be610b..63981e90 100644 --- a/docs/spec/ngdocSpec.js +++ b/docs/spec/ngdocSpec.js @@ -1,4 +1,5 @@  var ngdoc = require('ngdoc.js'); +var DOM = require('dom.js').DOM;  describe('ngdoc', function(){    var Doc = ngdoc.Doc; @@ -253,5 +254,67 @@ describe('ngdoc', function(){        });      });    }); +   +  describe('usage', function(){ +    var dom; +     +    beforeEach(function(){ +      dom = new DOM(); +      this.addMatchers({ +        toContain: function(text) {  +          this.actual = this.actual.toString(); +          return this.actual.indexOf(text) > -1;  +        } +      }); +    }); +     +    describe('filter', function(){ +      it('should format', function(){ +        var doc = new Doc({ +          ngdoc:'formatter', +          shortName:'myFilter', +          param: [ +            {name:'a'}, +            {name:'b'} +          ] +        }); +        doc.html_usage_filter(dom); +        expect(dom).toContain('myFilter_expression | myFilter:b'); +        expect(dom).toContain('angular.filter.myFilter(a, b)'); +      }); +    }); +     +    describe('validator', function(){ +      it('should format', function(){ +        var doc = new Doc({ +          ngdoc:'validator', +          shortName:'myValidator', +          param: [ +            {name:'a'}, +            {name:'b'} +          ] +        }); +        doc.html_usage_validator(dom); +        expect(dom).toContain('ng:validate="myValidator:b"'); +        expect(dom).toContain('angular.validator.myValidator(a, b)'); +      }); +    }); +     +    describe('formatter', function(){ +      it('should format', function(){ +        var doc = new Doc({ +          ngdoc:'formatter', +          shortName:'myFormatter', +          param: [ +            {name:'a'}, +          ] +        }); +        doc.html_usage_formatter(dom); +        expect(dom).toContain('ng:format="myFormatter:a"'); +        expect(dom).toContain('var userInputString = angular.formatter.myFormatter.format(modelValue, a);'); +        expect(dom).toContain('var modelValue = angular.formatter.myFormatter.parse(userInputString, a);'); +      }); +    }); +  });  }); diff --git a/docs/src/ngdoc.js b/docs/src/ngdoc.js index ac7d0bb1..cae24cc3 100644 --- a/docs/src/ngdoc.js +++ b/docs/src/ngdoc.js @@ -231,15 +231,7 @@ Doc.prototype = {        dom.code(function(){          dom.text(self.name);          dom.text('('); -        var first = true; -        (self.param || []).forEach(function(param){ -          if (first) { -            first = false; -          } else { -            dom.text(', '); -          } -          dom.text(param.name); -        }); +        self.parameters(dom, ', ');          dom.text(');');        }); @@ -273,44 +265,17 @@ Doc.prototype = {            dom.text(self.shortName);            dom.text('_expression | ');            dom.text(self.shortName); -          var first = true; -          (self.param||[]).forEach(function(param){ -            if (first) { -              first = false; -            } else { -              if (param.optional) { -                dom.tag('i', function(){ -                  dom.text('[:' + param.name + ']'); -                }); -              } else { -                dom.text(':' + param.name); -              } -            } -          }); +          self.parameters(dom, ':', true);            dom.text(' }}');          });        }); -      dom.h3('In JavaScript', function(){ +      dom.h('In JavaScript', function(){          dom.tag('code', function(){            dom.text('angular.filter.');            dom.text(self.shortName);            dom.text('('); -          var first = true; -          (self.param||[]).forEach(function(param){ -            if (first) { -              first = false; -              dom.text(param.name); -            } else { -              if (param.optional) { -                dom.tag('i', function(){ -                  dom.text('[, ' + param.name + ']'); -                }); -              } else { -                dom.text(', ' + param.name); -              } -            } -          }); +          self.parameters(dom, ', ');            dom.text(')');          });        }); @@ -319,32 +284,40 @@ Doc.prototype = {        self.html_usage_returns(dom);      });    }, -     +      html_usage_formatter: function(dom){      var self = this;      dom.h('Usage', function(){        dom.h('In HTML Template Binding', function(){          dom.code(function(){ -          dom.text('<input type="text" ng:format="'); +          if (self.inputType=='select') +            dom.text('<select name="bindExpression"'); +          else +            dom.text('<input type="text" name="bindExpression"'); +          dom.text(' ng:format="');            dom.text(self.shortName); +          self.parameters(dom, ':', false, true);            dom.text('">');          });        }); -      dom.h3('In JavaScript', function(){ +      dom.h('In JavaScript', function(){          dom.code(function(){            dom.text('var userInputString = angular.formatter.');            dom.text(self.shortName); -          dom.text('.format(modelValue);'); -        }); -        dom.html('<br/>'); -        dom.code(function(){ +          dom.text('.format(modelValue'); +          self.parameters(dom, ', ', false, true); +          dom.text(');'); +          dom.text('\n');            dom.text('var modelValue = angular.formatter.');            dom.text(self.shortName); -          dom.text('.parse(userInputString);'); +          dom.text('.parse(userInputString'); +          self.parameters(dom, ', ', false, true); +          dom.text(');');          });        }); +      self.html_usage_parameters(dom);        self.html_usage_returns(dom);        });    }, @@ -356,18 +329,7 @@ Doc.prototype = {          dom.code(function(){            dom.text('<input type="text" ng:validate="');            dom.text(self.shortName); -          var first = true; -          (self.param||[]).forEach(function(param){ -            if (first) { -              first = false; -            } else { -              if (param.optional) { -                dom.text('[:' + param.name + ']'); -              } else { -                dom.text(':' + param.name); -              } -            } -          }); +          self.parameters(dom, ':', true);            dom.text('"/>');          });        }); @@ -377,19 +339,7 @@ Doc.prototype = {            dom.text('angular.validator.');            dom.text(self.shortName);            dom.text('('); -          var first = true; -          (self.param||[]).forEach(function(param){ -            if (first) { -              first = false; -              dom.text(param.name); -            } else { -              if (param.optional) { -                dom.text('[, ' + param.name + ']'); -              } else { -                dom.text(', ' + param.name); -              } -            } -          }); +          self.parameters(dom, ', ');            dom.text(')');          });        }); @@ -443,8 +393,22 @@ Doc.prototype = {    },    html_usage_service: function(dom){ -  } +  }, +  parameters: function(dom, separator, skipFirst, prefix) { +    var sep = prefix ? separator : ''; +    (this.param||[]).forEach(function(param, i){ +      if (!(skipFirst && i==0)) { +        if (param.optional) { +          dom.text('[' + sep + param.name + ']'); +        } else { +          dom.text(sep + param.name); +        } +      } +      sep = separator; +    }); +  } +      };  ////////////////////////////////////////////////////////// diff --git a/src/JSON.js b/src/JSON.js index 0d23314f..9a2b34e5 100644 --- a/src/JSON.js +++ b/src/JSON.js @@ -33,7 +33,7 @@ function toJson(obj, pretty) {   * @returns {Object|Array|Date|string|number} Deserialized thingy.   */  function fromJson(json, useNative) { -  if (!json) return json; +  if (!isString(json)) return json;    var obj, p, expression; diff --git a/src/directives.js b/src/directives.js index 8584df8b..10d1f1e5 100644 --- a/src/directives.js +++ b/src/directives.js @@ -197,7 +197,7 @@ angularDirective("ng:bind", function(expression, element){        if (lastValue === value && lastError == error) return;        isDomElement = isElement(value);        if (!isHtml && !isDomElement && isObject(value)) { -        value = toJson(value); +        value = toJson(value, true);        }        if (value != lastValue || error != lastError) {          lastValue = value; @@ -234,7 +234,7 @@ function compileBindTemplate(template){          return text;        });      }); -    bindTemplateCache[template] = fn = function(element){ +    bindTemplateCache[template] = fn = function(element, prettyPrintJson){        var parts = [], self = this,           oldElement = this.hasOwnProperty($$element) ? self.$element : _undefined;        self.$element = element; @@ -243,7 +243,7 @@ function compileBindTemplate(template){          if (isElement(value))            value = '';          else if (isObject(value)) -          value = toJson(value, true); +          value = toJson(value, prettyPrintJson);          parts.push(value);        }        self.$element = oldElement; @@ -292,7 +292,7 @@ angularDirective("ng:bind-template", function(expression, element){    return function(element) {      var lastValue;      this.$onEval(function() { -      var value = templateFn.call(this, element); +      var value = templateFn.call(this, element, true);        if (value != lastValue) {          element.text(value);          lastValue = value; diff --git a/src/formatters.js b/src/formatters.js index 19b8df81..5e49ccf4 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -15,7 +15,7 @@ angularFormatter.noop = formatter(identity, identity);   * @description   *   Formats the user input as JSON text.   * - * @returns {string} A JSON string representation of the model. + * @returns {?string} A JSON string representation of the model.   *   * @example   * <div ng:init="data={name:'misko', project:'angular'}"> @@ -30,7 +30,9 @@ angularFormatter.noop = formatter(identity, identity);   *   expect(binding('data')).toEqual('data={\n  }');   * });   */ -angularFormatter.json = formatter(toJson, fromJson); +angularFormatter.json = formatter(toJson, function(value){ +  return fromJson(value || 'null'); +});  /**   * @workInProgress @@ -154,3 +156,59 @@ angularFormatter.list = formatter(  angularFormatter.trim = formatter(    function(obj) { return obj ? trim("" + obj) : ""; }  ); + +/** + * @workInProgress + * @ngdoc formatter + * @name angular.formatter.index + * @description + * Index formatter is meant to be used with `select` input widget. It is useful when one needs + * to select from a set of objects. To create pull-down one can iterate over the array of object + * to build the UI. However  the value of the pull-down must be a string. This means that when on + * object is selected form the pull-down, the pull-down value is a string which needs to be + * converted back to an object. This conversion from string to on object is not possible, at best + * the converted object is a copy of the original object. To solve this issue we create a pull-down + * where the value strings are an index of the object in the array. When pull-down is selected the + * index can be used to look up the original user object. + * + * @inputType select + * @param {array} array to be used for selecting an object. + * @returns {object} object which is located at the selected position. + * + * @example + * <script> + * function DemoCntl(){ + *   this.users = [ + *     {name:'guest', password:'guest'}, + *     {name:'user', password:'123'}, + *     {name:'admin', password:'abc'} + *   ]; + * } + * </script> + * <div ng:controller="DemoCntl"> + *   User: + *   <select name="currentUser" ng:format="index:users"> + *     <option ng:repeat="user in users" value="{{$index}}">{{user.name}}</option> + *   </select> + *   <select name="currentUser" ng:format="index:users"> + *     <option ng:repeat="user in users" value="{{$index}}">{{user.name}}</option> + *   </select> + *   user={{currentUser.name}}<br/> + *   password={{currentUser.password}}<br/> + * </div> + * + * @scenario + * it('should format trim', function(){ + *   expect(binding('currentUser.password')).toEqual('guest'); + *   select('currentUser').option('2'); + *   expect(binding('currentUser.password')).toEqual('abc'); + * }); + */ +angularFormatter.index = formatter( +  function(object, array){ +    return '' + indexOf(array || [], object); +  }, +  function(index, array){ +    return (array||[])[index]; +  } +); diff --git a/src/parser.js b/src/parser.js index 4227a6c8..ac62fb97 100644 --- a/src/parser.js +++ b/src/parser.js @@ -218,6 +218,7 @@ function parser(text, json){    var ZERO = valueFn(0),        tokens = lex(text, json),        assignment = _assignment,  +      assignable = logicalOR,        functionCall = _functionCall,         fieldAccess = _fieldAccess,         objectIndex = _objectIndex,  @@ -231,6 +232,7 @@ function parser(text, json){      functionCall =         fieldAccess =         objectIndex =  +      assignable =        filterChain =         functionIdent =         pipeFunction =  @@ -238,9 +240,11 @@ function parser(text, json){    }    return {        assertAllConsumed: assertAllConsumed, +      assignable: assignable,        primary: primary,        statements: statements,        validator: validator, +      formatter: formatter,        filter: filter,        //TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)        watch: watch @@ -353,6 +357,33 @@ function parser(text, json){      return pipeFunction(angularValidator);    } +  function formatter(){ +    var token = expect(); +    var formatter = angularFormatter[token.text]; +    var argFns = []; +    var token; +    if (!formatter) throwError('is not a valid formatter.', token); +    while(true) { +      if ((token = expect(':'))) { +        argFns.push(expression()); +      } else { +        return valueFn({ +          format:invokeFn(formatter.format),  +          parse:invokeFn(formatter.parse) +        }); +      } +    } +    function invokeFn(fn){ +      return function(self, input){ +        var args = [input]; +        for ( var i = 0; i < argFns.length; i++) { +          args.push(argFns[i](self)); +        } +        return fn.apply(self, args); +      }; +    } +  } +    function _pipeFunction(fnScope){      var fn = functionIdent(fnScope);      var argsFn = []; diff --git a/src/widgets.js b/src/widgets.js index 473b6f1f..e7f78971 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -8,16 +8,16 @@   * standard HTML set. These widgets are bound using the name attribute   * to an expression. In addition they can have `ng:validate`, `ng:required`,   * `ng:format`, `ng:change` attribute to further control their behavior. - *  + *   * @usageContent   *   see example below for usage - *  + *   *   <input type="text|checkbox|..." ... />   *   <textarea ... />   *   <select ...>   *     <option>...</option>   *   </select> - *  + *   * @example  <table style="font-size:.9em;">    <tr> @@ -96,7 +96,7 @@      <td><tt>{{input6|json}}</tt></td>    </tr>  </table> -  +   * @scenario   * it('should exercise text', function(){   *   input('input1').enter('Carlos'); @@ -134,14 +134,19 @@  function modelAccessor(scope, element) {    var expr = element.attr('name'); +  var assign;    if (expr) { +    assign = parser(expr).assignable().assign; +    if (!assign) throw new Error("Expression '" + expr + "' is not assignable.");      return {        get: function() {          return scope.$eval(expr);        },        set: function(value) {          if (value !== _undefined) { -          return scope.$tryEval(expr + '=' + toJson(value), element); +          return scope.$tryEval(function(){ +            assign(scope, value); +          }, element);          }        }      }; @@ -151,15 +156,14 @@ function modelAccessor(scope, element) {  function modelFormattedAccessor(scope, element) {    var accessor = modelAccessor(scope, element),        formatterName = element.attr('ng:format') || NOOP, -      formatter = angularFormatter(formatterName); -  if (!formatter) throw "Formatter named '" + formatterName + "' not found."; +      formatter = compileFormatter(formatterName);    if (accessor) {      return {        get: function() { -        return formatter.format(accessor.get()); +        return formatter.format(scope, accessor.get());        },        set: function(value) { -        return accessor.set(formatter.parse(value)); +        return accessor.set(formatter.parse(scope, value));        }      };    } @@ -169,6 +173,10 @@ function compileValidator(expr) {    return parser(expr).validator()();  } +function compileFormatter(expr) { +  return parser(expr).formatter()(); +} +  /**   * @workInProgress   * @ngdoc widget @@ -195,7 +203,7 @@ function compileValidator(expr) {      I need an integer or nothing:      <input type="text" name="value" ng:validate="integer"><br/> - *  + *   * @scenario     it('should check ng:validate', function(){       expect(element('.doc-example-live :input:last').attr('className')). @@ -214,7 +222,7 @@ function compileValidator(expr) {   * @description   * The `ng:required` attribute widget validates that the user input is present. It is a special case   * of the {@link angular.widget.@ng:validate ng:validate} attribute widget. - *  + *   * @element INPUT   * @css ng-validation-error   * @@ -253,10 +261,10 @@ function compileValidator(expr) {   * array.   *   * @example -    Enter a comma separated list of items:  +    Enter a comma separated list of items:      <input type="text" name="list" ng:format="list" value="table, chairs, plate">      <pre>list={{list}}</pre> - *  + *   * @scenario     it('should check ng:format', function(){       expect(binding('list')).toBe('list=["table","chairs","plate"]'); @@ -269,11 +277,10 @@ function valueAccessor(scope, element) {        validator = compileValidator(validatorName),        requiredExpr = element.attr('ng:required'),        formatterName = element.attr('ng:format') || NOOP, -      formatter = angularFormatter(formatterName), +      formatter = compileFormatter(formatterName),        format, parse, lastError, required,        invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop};    if (!validator) throw "Validator named '" + validatorName + "' not found."; -  if (!formatter) throw "Formatter named '" + formatterName + "' not found.";    format = formatter.format;    parse = formatter.parse;    if (requiredExpr) { @@ -291,7 +298,7 @@ function valueAccessor(scope, element) {        if (lastError)          elementError(element, NG_VALIDATION_ERROR, _null);        try { -        var value = parse(element.val()); +        var value = parse(scope, element.val());          validate();          return value;        } catch (e) { @@ -301,7 +308,7 @@ function valueAccessor(scope, element) {      },      set: function(value) {        var oldValue = element.val(), -          newValue = format(value); +          newValue = format(scope, value);        if (oldValue != newValue) {          element.val(newValue || ''); // needed for ie        } @@ -355,19 +362,22 @@ function radioAccessor(scope, element) {  }  function optionsAccessor(scope, element) { -  var options = element[0].options; +  var formatterName = element.attr('ng:format') || NOOP, +      formatter = compileFormatter(formatterName);    return {      get: function(){        var values = []; -      forEach(options, function(option){ -        if (option.selected) values.push(option.value); +      forEach(element[0].options, function(option){ +        if (option.selected) values.push(formatter.parse(scope, option.value));        });        return values;      },      set: function(values){        var keys = {}; -      forEach(values, function(value){ keys[value] = true; }); -      forEach(options, function(option){ +      forEach(values, function(value){ +        keys[formatter.format(scope, value)] = true; +      }); +      forEach(element[0].options, function(option){          option.selected = keys[option.value];        });      } @@ -376,6 +386,18 @@ function optionsAccessor(scope, element) {  function noopAccessor() { return { get: noop, set: noop }; } +/* + * TODO: refactor + * + * The table bellow is not quite right. In some cases the formatter is on the model side + * and in some cases it is on the view side. This is a historical artifact + * + * The concept of model/view accessor is useful for anyone who is trying to develop UI, and + * so it should be exposed to others. There should be a form object which keeps track of the + * accessors and also acts as their factory. It should expose it as an object and allow + * the validator to publish errors to it, so that the the error messages can be bound to it. + * + */  var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true),      buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop),      INPUT_TYPE = { @@ -389,8 +411,8 @@ var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, ini        'image':           buttonWidget,        'checkbox':        inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)),        'radio':           inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), -      'select-one':      inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(_null)), -      'select-multiple': inputWidget('change', modelFormattedAccessor, optionsAccessor, initWidgetValue([])) +      'select-one':      inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(_null)), +      'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([]))  //      'file':            fileWidget???      }; @@ -427,7 +449,7 @@ function radioInit(model, view, element) {   *   * @description   * The directive executes an expression whenever the input widget changes. - *  + *   * @element INPUT   * @param {expression} expression to execute.   * @@ -438,17 +460,17 @@ function radioInit(model, view, element) {         changeCount {{textCount}}<br/>      <input type="checkbox" name="checkbox" ng:change="checkboxCount = 1 + checkboxCount">         changeCount {{checkboxCount}}<br/> - *  + *   * @scenario     it('should check ng:change', function(){       expect(binding('textCount')).toBe('0');       expect(binding('checkboxCount')).toBe('0'); -      +       using('.doc-example-live').input('text').enter('abc');       expect(binding('textCount')).toBe('1');       expect(binding('checkboxCount')).toBe('0'); -      -      + +       using('.doc-example-live').input('checkbox').check();       expect(binding('textCount')).toBe('1');       expect(binding('checkboxCount')).toBe('1'); @@ -504,41 +526,49 @@ angularWidget('select', function(element){   * <select name="selection">   *   <option ng:repeat="x in [1,2]">{{x}}</option>   * </select> - *  + *   * The issue is that the select gets evaluated before option is unrolled.   * This means that the selection is undefined, but the browser - * default behavior is to show the top selection in the list.  + * default behavior is to show the top selection in the list.   * To fix that we register a $update function on the select element - * and the option creation then calls the $update function when it is  - * unrolled. The $update function then calls this update function, which  + * and the option creation then calls the $update function when it is + * unrolled. The $update function then calls this update function, which   * then tries to determine if the model is unassigned, and if so it tries to   * chose one of the options from the list.   */  angularWidget('option', function(){    this.descend(true);    this.directives(true); -  return function(element) { -    var select = element.parent(); +  return function(option) { +    var select = option.parent(); +    var isMultiple = select.attr('multiple') == '';      var scope = retrieveScope(select); -    var model = modelFormattedAccessor(scope, select); -    var view = valueAccessor(scope, select); -    var option = element; +    var model = modelAccessor(scope, select); +    var formattedModel = modelFormattedAccessor(scope, select); +    var view = isMultiple +      ? optionsAccessor(scope, select) +      : valueAccessor(scope, select);      var lastValue = option.attr($value); -    var lastSelected = option.attr('ng-' + $selected); -    element.data($$update, function(){ -      var value = option.attr($value); -      var selected = option.attr('ng-' + $selected); -      var modelValue = model.get(); -      if (lastSelected != selected || lastValue != value) { -        lastSelected = selected; -        lastValue = value; -        if (selected || modelValue == _null || modelValue == _undefined)  -          model.set(value); -        if (value == modelValue) { -          view.set(lastValue); +    var wasSelected = option.attr('ng-' + $selected); +    option.data($$update, isMultiple +      ? function(){ +          view.set(model.get());          } -      } -    }); +      : function(){ +          var currentValue = option.attr($value); +          var isSelected = option.attr('ng-' + $selected); +          var modelValue = model.get(); +          if (wasSelected != isSelected || lastValue != currentValue) { +            wasSelected = isSelected; +            lastValue = currentValue; +            if (isSelected || !modelValue == null || modelValue == undefined ) +              formattedModel.set(currentValue); +            if (currentValue == modelValue) { +              view.set(lastValue); +            } +          } +        } +    );    };  }); @@ -549,12 +579,12 @@ angularWidget('option', function(){   *   * @description   * Include external HTML fragment. - *  - * Keep in mind that Same Origin Policy applies to included resources  + * + * Keep in mind that Same Origin Policy applies to included resources   * (e.g. ng:include won't work for file:// access).   *   * @param {string} src expression evaluating to URL. - * @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an  + * @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an   *                 instance of angular.scope to set the HTML fragment to.   * @param {string=} onload Expression to evaluate when a new partial is loaded.   * @@ -636,17 +666,17 @@ angularWidget('ng:include', function(element){   *   * @description   * Conditionally change the DOM structure. - *  + *   * @usageContent   * <any ng:switch-when="matchValue1">...</any>   *   <any ng:switch-when="matchValue2">...</any>   *   ...   *   <any ng:switch-default>...</any> - *  + *   * @param {*} on expression to match against <tt>ng:switch-when</tt>. - * @paramDescription  + * @paramDescription   * On child elments add: - *  + *   * * `ng:switch-when`: the case statement to match against. If match then this   *   case will be displayed.   * * `ng:switch-default`: the default case when no other casses match. diff --git a/test/FormattersSpec.js b/test/FormattersSpec.js index af50f384..1ebd8e22 100644 --- a/test/FormattersSpec.js +++ b/test/FormattersSpec.js @@ -33,5 +33,20 @@ describe("formatter", function(){      assertEquals('a', angular.formatter.trim.format(" a "));      assertEquals('a', angular.formatter.trim.parse(' a '));    }); +   +  describe('json', function(){ +    it('should treat empty string as null', function(){ +      expect(angular.formatter.json.parse('')).toEqual(null); +    }); +  }); +   +  describe('index', function(){ +    it('should parse an object from array', function(){ +      expect(angular.formatter.index.parse('1', ['A', 'B', 'C'])).toEqual('B'); +    }); +    it('should format an index from array', function(){ +      expect(angular.formatter.index.format('B', ['A', 'B', 'C'])).toEqual('1'); +    }); +  });  }); diff --git a/test/JsonSpec.js b/test/JsonSpec.js index 6d8a40e4..4a160905 100644 --- a/test/JsonSpec.js +++ b/test/JsonSpec.js @@ -92,11 +92,11 @@ describe('json', function(){    it('should not serialize undefined values', function() {      expect(angular.toJson({A:undefined})).toEqual('{}');    }); -   +    it('should not serialize $window object', function() {      expect(toJson(window)).toEqual('WINDOW');    }); -   +    it('should not serialize $document object', function() {      expect(toJson(document)).toEqual('DOCUMENT');    }); @@ -116,6 +116,13 @@ describe('json', function(){      expect(fromJson("{exp:1.2e-10}")).toEqual({exp:1.2E-10});    }); +  it('should ignore non-strings', function(){ +    expect(fromJson([])).toEqual([]); +    expect(fromJson({})).toEqual({}); +    expect(fromJson(null)).toEqual(null); +    expect(fromJson(undefined)).toEqual(undefined); +  }); +    //run these tests only in browsers that have native JSON parser    if (JSON && JSON.parse) { @@ -187,18 +194,18 @@ describe('json', function(){        expect(function(){fromJson('[].constructor');}).          toThrow(new Error("Parse Error: Token '.' is not valid json at column 3 of expression [[].constructor] starting at [.constructor]."));      }); -     +      it('should not allow object dereference', function(){        expect(function(){fromJson('{a:1, b: $location, c:1}');}).toThrow();        expect(function(){fromJson("{a:1, b:[1]['__parent__']['location'], c:1}");}).toThrow();      }); -     +      it('should not allow assignments', function(){        expect(function(){fromJson("{a:1, b:[1]=1, c:1}");}).toThrow();        expect(function(){fromJson("{a:1, b:=1, c:1}");}).toThrow();        expect(function(){fromJson("{a:1, b:x=1, c:1}");}).toThrow();      }); -     +    });  }); diff --git a/test/ParserSpec.js b/test/ParserSpec.js index c237aa40..4d0e14dc 100644 --- a/test/ParserSpec.js +++ b/test/ParserSpec.js @@ -396,4 +396,29 @@ describe('parser', function() {      expect(scope.obj.name).toBeUndefined();      expect(scope.obj[0].name).toEqual(1);    }); +   +  describe('formatter', function(){ +    it('should return no argument function', function() { +      var noop = parser('noop').formatter()(); +      expect(noop.format(null, 'abc')).toEqual('abc'); +      expect(noop.parse(null, '123')).toEqual('123'); +    }); +     +    it('should delegate arguments', function(){ +      var index = parser('index:objs').formatter()(); +      expect(index.format({objs:['A','B']}, 'B')).toEqual('1'); +      expect(index.parse({objs:['A','B']}, '1')).toEqual('B'); +    }); +  }); +   +  describe('assignable', function(){ +    it('should expose assignment function', function(){ +      var fn = parser('a').assignable(); +      expect(fn.assign).toBeTruthy(); +      var scope = {}; +      fn.assign(scope, 123); +      expect(scope).toEqual({a:123}); +    }); +  }); +    }); diff --git a/test/directivesSpec.js b/test/directivesSpec.js index ab1813c3..ef8241f1 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -104,10 +104,17 @@ describe("directive", function(){    }); -  it('should ng:bind-attr', function(){ -    var scope = compile('<img ng:bind-attr="{src:\'http://localhost/mysrc\', alt:\'myalt\'}"/>'); -    expect(element.attr('src')).toEqual('http://localhost/mysrc'); -    expect(element.attr('alt')).toEqual('myalt'); +  describe('ng:bind-attr', function(){ +    it('should bind attributes', function(){ +      var scope = compile('<img ng:bind-attr="{src:\'http://localhost/mysrc\', alt:\'myalt\'}"/>'); +      expect(element.attr('src')).toEqual('http://localhost/mysrc'); +      expect(element.attr('alt')).toEqual('myalt'); +    }); +     +    it('should not pretty print JSON in attributes', function(){ +      var scope = compile('<img alt="{{ {a:1} }}"/>'); +      expect(element.attr('alt')).toEqual('{"a":1}'); +    });    });    it('should remove special attributes on false', function(){ diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 8dab4630..946c433f 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -44,7 +44,7 @@ describe("widget", function(){          expect(scope.$get('name')).toEqual('Kai');          expect(scope.$get('count')).toEqual(2);        }); -       +        it('should not trigger eval if value does not change', function(){          compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>');          expect(scope.name).toEqual("Misko"); @@ -53,7 +53,7 @@ describe("widget", function(){          expect(scope.name).toEqual("Misko");          expect(scope.count).toEqual(0);        }); -       +        it('should allow complex refernce binding', function(){          compile('<div ng:init="obj={abc:{}}">'+                    '<input type="Text" name="obj[\'abc\'].name" value="Misko""/>'+ @@ -416,7 +416,7 @@ describe("widget", function(){          scope.$eval();          expect(element[0].childNodes[1].selected).toEqual(true);        }); -       +        it('should select default option on repeater', function(){          compile(              '<select name="selection">' + @@ -424,7 +424,7 @@ describe("widget", function(){              '</select>');          expect(scope.selection).toEqual('1');        }); -       +        it('should select selected option on repeater', function(){          compile(              '<select name="selection">' + @@ -433,7 +433,7 @@ describe("widget", function(){              '</select>');          expect(scope.selection).toEqual('ABC');        }); -       +        it('should select dynamically selected option on repeater', function(){          compile(              '<select name="selection">' + @@ -441,21 +441,81 @@ describe("widget", function(){              '</select>');          expect(scope.selection).toEqual('2');        }); -       + +      it('should allow binding to objects through JSON', function(){ +        compile( +            '<select name="selection" ng:format="json">' + +              '<option ng:repeat="obj in objs" value="{{obj}}">{{obj.name}}</option>' + +            '</select>'); +        scope.objs = [{name:'A'}, {name:'B'}]; +        scope.$eval(); +        expect(scope.selection).toEqual({name:'A'}); +      }); + +      it('should allow binding to objects through index', function(){ +        compile( +            '<select name="selection" ng:format="index:objs">' + +              '<option ng:repeat="obj in objs" value="{{$index}}">{{obj.name}}</option>' + +            '</select>'); +        scope.objs = [{name:'A'}, {name:'B'}]; +        scope.$eval(); +        expect(scope.selection).toBe(scope.objs[0]); +      }); +      }); -    it('should support type="select-multiple"', function(){ -      compile( -        '<select name="selection" multiple>' + -          '<option>A</option>' + -          '<option selected>B</option>' + -        '</select>'); -      expect(scope.selection).toEqual(['B']); -      scope.selection = ['A']; -      scope.$eval(); -      expect(element[0].childNodes[0].selected).toEqual(true); +    describe('select-multiple', function(){ +      it('should support type="select-multiple"', function(){ +        compile('<select name="selection" multiple>' + +                  '<option>A</option>' + +                  '<option selected>B</option>' + +                '</select>'); +        expect(scope.selection).toEqual(['B']); +        scope.selection = ['A']; +        scope.$eval(); +        expect(element[0].childNodes[0].selected).toEqual(true); +      }); + +      it('should allow binding to objects through index', function(){ +        compile('<select name="selection" multiple ng:format="index:list">' + +                  '<option selected value="0">A</option>' + +                  '<option selected value="1">B</option>' + +                  '<option value="2">C</option>' + +                '</select>', +                function(){ +                  scope.list = [{name:'A'}, {name:'B'}, {name:'C'}]; +                }); +        scope.$eval(); +        expect(scope.selection).toEqual([{name:'A'}, {name:'B'}]); +      }); + +      it('should be empty array when no items are selected', function(){ +        compile( +          '<select name="selection" multiple ng:format="index:list">' + +            '<option value="0">A</option>' + +            '<option value="1">B</option>' + +            '<option value="2">C</option>' + +          '</select>'); +        scope.list = [{name:'A'}, {name:'B'}, {name:'C'}]; +        scope.$eval(); +        expect(scope.selection).toEqual([]); +      }); + +      it('should be contain the selected object', function(){ +        compile('<select name="selection" multiple ng:format="index:list">' + +                  '<option value="0">A</option>' + +                  '<option value="1" selected>B</option>' + +                  '<option value="2">C</option>' + +                '</select>', +                function(){ +                  scope.list = [{name:'A'}, {name:'B'}, {name:'C'}]; +                }); +        scope.$eval(); +        expect(scope.selection).toEqual([{name:'B'}]); +      }); +      }); -     +      it('should ignore text widget which have no name', function(){        compile('<input type="text"/>');        expect(scope.$element.attr('ng-exception')).toBeFalsy(); @@ -504,7 +564,7 @@ describe("widget", function(){        scope.$eval();        expect(element.text()).toEqual('true:misko');      }); -     +      it("should compare stringified versions", function(){        var switchWidget = angular.widget('ng:switch');        expect(switchWidget.equals(true, 'true')).toEqual(true); @@ -521,7 +581,7 @@ describe("widget", function(){        scope.$eval();        expect(element.text()).toEqual('one');      }); -     +      it("should match urls", function(){        var scope = angular.compile('<ng:switch on="url" using="route:params"><div ng:switch-when="/Book/:name">{{params.name}}</div></ng:switch>');        scope.url = '/Book/Moby'; | 
