diff options
| -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'; |
