From 347be5ae9aa6829427e1e8e1b1e58afdf2a36c0a Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 13 Jan 2011 10:35:26 -0800 Subject: 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. --- CHANGELOG.md | 10 ++-- docs/spec/ngdocSpec.js | 63 +++++++++++++++++++++ docs/src/ngdoc.js | 110 +++++++++++++----------------------- src/JSON.js | 2 +- src/directives.js | 8 +-- src/formatters.js | 62 ++++++++++++++++++++- src/parser.js | 31 +++++++++++ src/widgets.js | 148 +++++++++++++++++++++++++++++-------------------- test/FormattersSpec.js | 15 +++++ test/JsonSpec.js | 17 ++++-- test/ParserSpec.js | 25 +++++++++ test/directivesSpec.js | 15 +++-- 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 @@ # 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. # 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(''); }); }); - 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('
'); - 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(''); }); }); @@ -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 *
@@ -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 + * + *
+ * User: + * + * + * user={{currentUser.name}}
+ * password={{currentUser.password}}
+ *
+ * + * @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 - * + * * *