diff options
Diffstat (limited to 'src/widgets.js')
| -rw-r--r-- | src/widgets.js | 148 |
1 files changed, 89 insertions, 59 deletions
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. |
