aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMisko Hevery2011-01-13 10:35:26 -0800
committerMisko Hevery2011-01-14 10:30:00 -0800
commit347be5ae9aa6829427e1e8e1b1e58afdf2a36c0a (patch)
tree3b350a12378c1ec63f60cce0fe674186d204726e
parent934f44f69e94a77a3ea6c19dc5c6f82ade2cc669 (diff)
downloadangular.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.md10
-rw-r--r--docs/spec/ngdocSpec.js63
-rw-r--r--docs/src/ngdoc.js110
-rw-r--r--src/JSON.js2
-rw-r--r--src/directives.js8
-rw-r--r--src/formatters.js62
-rw-r--r--src/parser.js31
-rw-r--r--src/widgets.js148
-rw-r--r--test/FormattersSpec.js15
-rw-r--r--test/JsonSpec.js17
-rw-r--r--test/ParserSpec.js25
-rw-r--r--test/directivesSpec.js15
-rw-r--r--test/widgetsSpec.js98
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';