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 /src | |
| 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.
Diffstat (limited to 'src')
| -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 |
5 files changed, 185 insertions, 66 deletions
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. |
