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.
This commit is contained in:
Misko Hevery 2011-01-13 10:35:26 -08:00
parent 934f44f69e
commit 347be5ae9a
13 changed files with 433 additions and 171 deletions

View file

@ -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

View file

@ -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);');
});
});
});
});

View file

@ -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;
});
}
};
//////////////////////////////////////////////////////////

View file

@ -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;

View file

@ -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;

View file

@ -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];
}
);

View file

@ -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 = [];

View file

@ -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.

View file

@ -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');
});
});
});

View file

@ -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();
});
});
});

View file

@ -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});
});
});
});

View file

@ -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(){

View file

@ -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';