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) # # <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) # # <angular/> 0.9.9 time-shift (2011-01-13) #

View file

@ -1,4 +1,5 @@
var ngdoc = require('ngdoc.js'); var ngdoc = require('ngdoc.js');
var DOM = require('dom.js').DOM;
describe('ngdoc', function(){ describe('ngdoc', function(){
var Doc = ngdoc.Doc; var Doc = ngdoc.Doc;
@ -254,4 +255,66 @@ 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.code(function(){
dom.text(self.name); dom.text(self.name);
dom.text('('); dom.text('(');
var first = true; self.parameters(dom, ', ');
(self.param || []).forEach(function(param){
if (first) {
first = false;
} else {
dom.text(', ');
}
dom.text(param.name);
});
dom.text(');'); dom.text(');');
}); });
@ -273,44 +265,17 @@ Doc.prototype = {
dom.text(self.shortName); dom.text(self.shortName);
dom.text('_expression | '); dom.text('_expression | ');
dom.text(self.shortName); dom.text(self.shortName);
var first = true; self.parameters(dom, ':', 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);
}
}
});
dom.text(' }}'); dom.text(' }}');
}); });
}); });
dom.h3('In JavaScript', function(){ dom.h('In JavaScript', function(){
dom.tag('code', function(){ dom.tag('code', function(){
dom.text('angular.filter.'); dom.text('angular.filter.');
dom.text(self.shortName); dom.text(self.shortName);
dom.text('('); dom.text('(');
var first = true; self.parameters(dom, ', ');
(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);
}
}
});
dom.text(')'); dom.text(')');
}); });
}); });
@ -325,26 +290,34 @@ Doc.prototype = {
dom.h('Usage', function(){ dom.h('Usage', function(){
dom.h('In HTML Template Binding', function(){ dom.h('In HTML Template Binding', function(){
dom.code(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); dom.text(self.shortName);
self.parameters(dom, ':', false, true);
dom.text('">'); dom.text('">');
}); });
}); });
dom.h3('In JavaScript', function(){ dom.h('In JavaScript', function(){
dom.code(function(){ dom.code(function(){
dom.text('var userInputString = angular.formatter.'); dom.text('var userInputString = angular.formatter.');
dom.text(self.shortName); dom.text(self.shortName);
dom.text('.format(modelValue);'); dom.text('.format(modelValue');
}); self.parameters(dom, ', ', false, true);
dom.html('<br/>'); dom.text(');');
dom.code(function(){ dom.text('\n');
dom.text('var modelValue = angular.formatter.'); dom.text('var modelValue = angular.formatter.');
dom.text(self.shortName); 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); self.html_usage_returns(dom);
}); });
}, },
@ -356,18 +329,7 @@ Doc.prototype = {
dom.code(function(){ dom.code(function(){
dom.text('<input type="text" ng:validate="'); dom.text('<input type="text" ng:validate="');
dom.text(self.shortName); dom.text(self.shortName);
var first = true; self.parameters(dom, ':', true);
(self.param||[]).forEach(function(param){
if (first) {
first = false;
} else {
if (param.optional) {
dom.text('[:' + param.name + ']');
} else {
dom.text(':' + param.name);
}
}
});
dom.text('"/>'); dom.text('"/>');
}); });
}); });
@ -377,19 +339,7 @@ Doc.prototype = {
dom.text('angular.validator.'); dom.text('angular.validator.');
dom.text(self.shortName); dom.text(self.shortName);
dom.text('('); dom.text('(');
var first = true; self.parameters(dom, ', ');
(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);
}
}
});
dom.text(')'); dom.text(')');
}); });
}); });
@ -443,6 +393,20 @@ Doc.prototype = {
}, },
html_usage_service: function(dom){ 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. * @returns {Object|Array|Date|string|number} Deserialized thingy.
*/ */
function fromJson(json, useNative) { function fromJson(json, useNative) {
if (!json) return json; if (!isString(json)) return json;
var obj, p, expression; var obj, p, expression;

View file

@ -197,7 +197,7 @@ angularDirective("ng:bind", function(expression, element){
if (lastValue === value && lastError == error) return; if (lastValue === value && lastError == error) return;
isDomElement = isElement(value); isDomElement = isElement(value);
if (!isHtml && !isDomElement && isObject(value)) { if (!isHtml && !isDomElement && isObject(value)) {
value = toJson(value); value = toJson(value, true);
} }
if (value != lastValue || error != lastError) { if (value != lastValue || error != lastError) {
lastValue = value; lastValue = value;
@ -234,7 +234,7 @@ function compileBindTemplate(template){
return text; return text;
}); });
}); });
bindTemplateCache[template] = fn = function(element){ bindTemplateCache[template] = fn = function(element, prettyPrintJson){
var parts = [], self = this, var parts = [], self = this,
oldElement = this.hasOwnProperty($$element) ? self.$element : _undefined; oldElement = this.hasOwnProperty($$element) ? self.$element : _undefined;
self.$element = element; self.$element = element;
@ -243,7 +243,7 @@ function compileBindTemplate(template){
if (isElement(value)) if (isElement(value))
value = ''; value = '';
else if (isObject(value)) else if (isObject(value))
value = toJson(value, true); value = toJson(value, prettyPrintJson);
parts.push(value); parts.push(value);
} }
self.$element = oldElement; self.$element = oldElement;
@ -292,7 +292,7 @@ angularDirective("ng:bind-template", function(expression, element){
return function(element) { return function(element) {
var lastValue; var lastValue;
this.$onEval(function() { this.$onEval(function() {
var value = templateFn.call(this, element); var value = templateFn.call(this, element, true);
if (value != lastValue) { if (value != lastValue) {
element.text(value); element.text(value);
lastValue = value; lastValue = value;

View file

@ -15,7 +15,7 @@ angularFormatter.noop = formatter(identity, identity);
* @description * @description
* Formats the user input as JSON text. * 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 * @example
* <div ng:init="data={name:'misko', project:'angular'}"> * <div ng:init="data={name:'misko', project:'angular'}">
@ -30,7 +30,9 @@ angularFormatter.noop = formatter(identity, identity);
* expect(binding('data')).toEqual('data={\n }'); * expect(binding('data')).toEqual('data={\n }');
* }); * });
*/ */
angularFormatter.json = formatter(toJson, fromJson); angularFormatter.json = formatter(toJson, function(value){
return fromJson(value || 'null');
});
/** /**
* @workInProgress * @workInProgress
@ -154,3 +156,59 @@ angularFormatter.list = formatter(
angularFormatter.trim = formatter( angularFormatter.trim = formatter(
function(obj) { return obj ? trim("" + obj) : ""; } 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), var ZERO = valueFn(0),
tokens = lex(text, json), tokens = lex(text, json),
assignment = _assignment, assignment = _assignment,
assignable = logicalOR,
functionCall = _functionCall, functionCall = _functionCall,
fieldAccess = _fieldAccess, fieldAccess = _fieldAccess,
objectIndex = _objectIndex, objectIndex = _objectIndex,
@ -231,6 +232,7 @@ function parser(text, json){
functionCall = functionCall =
fieldAccess = fieldAccess =
objectIndex = objectIndex =
assignable =
filterChain = filterChain =
functionIdent = functionIdent =
pipeFunction = pipeFunction =
@ -238,9 +240,11 @@ function parser(text, json){
} }
return { return {
assertAllConsumed: assertAllConsumed, assertAllConsumed: assertAllConsumed,
assignable: assignable,
primary: primary, primary: primary,
statements: statements, statements: statements,
validator: validator, validator: validator,
formatter: formatter,
filter: filter, filter: filter,
//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular) //TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)
watch: watch watch: watch
@ -353,6 +357,33 @@ function parser(text, json){
return pipeFunction(angularValidator); 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){ function _pipeFunction(fnScope){
var fn = functionIdent(fnScope); var fn = functionIdent(fnScope);
var argsFn = []; var argsFn = [];

View file

@ -134,14 +134,19 @@
function modelAccessor(scope, element) { function modelAccessor(scope, element) {
var expr = element.attr('name'); var expr = element.attr('name');
var assign;
if (expr) { if (expr) {
assign = parser(expr).assignable().assign;
if (!assign) throw new Error("Expression '" + expr + "' is not assignable.");
return { return {
get: function() { get: function() {
return scope.$eval(expr); return scope.$eval(expr);
}, },
set: function(value) { set: function(value) {
if (value !== _undefined) { 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) { function modelFormattedAccessor(scope, element) {
var accessor = modelAccessor(scope, element), var accessor = modelAccessor(scope, element),
formatterName = element.attr('ng:format') || NOOP, formatterName = element.attr('ng:format') || NOOP,
formatter = angularFormatter(formatterName); formatter = compileFormatter(formatterName);
if (!formatter) throw "Formatter named '" + formatterName + "' not found.";
if (accessor) { if (accessor) {
return { return {
get: function() { get: function() {
return formatter.format(accessor.get()); return formatter.format(scope, accessor.get());
}, },
set: function(value) { 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()(); return parser(expr).validator()();
} }
function compileFormatter(expr) {
return parser(expr).formatter()();
}
/** /**
* @workInProgress * @workInProgress
* @ngdoc widget * @ngdoc widget
@ -269,11 +277,10 @@ function valueAccessor(scope, element) {
validator = compileValidator(validatorName), validator = compileValidator(validatorName),
requiredExpr = element.attr('ng:required'), requiredExpr = element.attr('ng:required'),
formatterName = element.attr('ng:format') || NOOP, formatterName = element.attr('ng:format') || NOOP,
formatter = angularFormatter(formatterName), formatter = compileFormatter(formatterName),
format, parse, lastError, required, format, parse, lastError, required,
invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop}; invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop};
if (!validator) throw "Validator named '" + validatorName + "' not found."; if (!validator) throw "Validator named '" + validatorName + "' not found.";
if (!formatter) throw "Formatter named '" + formatterName + "' not found.";
format = formatter.format; format = formatter.format;
parse = formatter.parse; parse = formatter.parse;
if (requiredExpr) { if (requiredExpr) {
@ -291,7 +298,7 @@ function valueAccessor(scope, element) {
if (lastError) if (lastError)
elementError(element, NG_VALIDATION_ERROR, _null); elementError(element, NG_VALIDATION_ERROR, _null);
try { try {
var value = parse(element.val()); var value = parse(scope, element.val());
validate(); validate();
return value; return value;
} catch (e) { } catch (e) {
@ -301,7 +308,7 @@ function valueAccessor(scope, element) {
}, },
set: function(value) { set: function(value) {
var oldValue = element.val(), var oldValue = element.val(),
newValue = format(value); newValue = format(scope, value);
if (oldValue != newValue) { if (oldValue != newValue) {
element.val(newValue || ''); // needed for ie element.val(newValue || ''); // needed for ie
} }
@ -355,19 +362,22 @@ function radioAccessor(scope, element) {
} }
function optionsAccessor(scope, element) { function optionsAccessor(scope, element) {
var options = element[0].options; var formatterName = element.attr('ng:format') || NOOP,
formatter = compileFormatter(formatterName);
return { return {
get: function(){ get: function(){
var values = []; var values = [];
forEach(options, function(option){ forEach(element[0].options, function(option){
if (option.selected) values.push(option.value); if (option.selected) values.push(formatter.parse(scope, option.value));
}); });
return values; return values;
}, },
set: function(values){ set: function(values){
var keys = {}; var keys = {};
forEach(values, function(value){ keys[value] = true; }); forEach(values, function(value){
forEach(options, function(option){ keys[formatter.format(scope, value)] = true;
});
forEach(element[0].options, function(option){
option.selected = keys[option.value]; option.selected = keys[option.value];
}); });
} }
@ -376,6 +386,18 @@ function optionsAccessor(scope, element) {
function noopAccessor() { return { get: noop, set: noop }; } 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), var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true),
buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop),
INPUT_TYPE = { INPUT_TYPE = {
@ -389,8 +411,8 @@ var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, ini
'image': buttonWidget, 'image': buttonWidget,
'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)), 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)),
'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit),
'select-one': inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(_null)), 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(_null)),
'select-multiple': inputWidget('change', modelFormattedAccessor, optionsAccessor, initWidgetValue([])) 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([]))
// 'file': fileWidget??? // 'file': fileWidget???
}; };
@ -517,28 +539,36 @@ angularWidget('select', function(element){
angularWidget('option', function(){ angularWidget('option', function(){
this.descend(true); this.descend(true);
this.directives(true); this.directives(true);
return function(element) { return function(option) {
var select = element.parent(); var select = option.parent();
var isMultiple = select.attr('multiple') == '';
var scope = retrieveScope(select); var scope = retrieveScope(select);
var model = modelFormattedAccessor(scope, select); var model = modelAccessor(scope, select);
var view = valueAccessor(scope, select); var formattedModel = modelFormattedAccessor(scope, select);
var option = element; var view = isMultiple
? optionsAccessor(scope, select)
: valueAccessor(scope, select);
var lastValue = option.attr($value); var lastValue = option.attr($value);
var lastSelected = option.attr('ng-' + $selected); var wasSelected = option.attr('ng-' + $selected);
element.data($$update, function(){ option.data($$update, isMultiple
var value = option.attr($value); ? function(){
var selected = option.attr('ng-' + $selected); view.set(model.get());
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);
} }
} : 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);
}
}
}
);
}; };
}); });

View file

@ -34,4 +34,19 @@ describe("formatter", function(){
assertEquals('a', angular.formatter.trim.parse(' 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

@ -116,6 +116,13 @@ describe('json', function(){
expect(fromJson("{exp:1.2e-10}")).toEqual({exp:1.2E-10}); 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 //run these tests only in browsers that have native JSON parser
if (JSON && JSON.parse) { if (JSON && JSON.parse) {

View file

@ -396,4 +396,29 @@ describe('parser', function() {
expect(scope.obj.name).toBeUndefined(); expect(scope.obj.name).toBeUndefined();
expect(scope.obj[0].name).toEqual(1); 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(){ describe('ng:bind-attr', function(){
var scope = compile('<img ng:bind-attr="{src:\'http://localhost/mysrc\', alt:\'myalt\'}"/>'); it('should bind attributes', function(){
expect(element.attr('src')).toEqual('http://localhost/mysrc'); var scope = compile('<img ng:bind-attr="{src:\'http://localhost/mysrc\', alt:\'myalt\'}"/>');
expect(element.attr('alt')).toEqual('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(){ it('should remove special attributes on false', function(){

View file

@ -442,18 +442,78 @@ describe("widget", function(){
expect(scope.selection).toEqual('2'); 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(){ describe('select-multiple', function(){
compile( it('should support type="select-multiple"', function(){
'<select name="selection" multiple>' + compile('<select name="selection" multiple>' +
'<option>A</option>' + '<option>A</option>' +
'<option selected>B</option>' + '<option selected>B</option>' +
'</select>'); '</select>');
expect(scope.selection).toEqual(['B']); expect(scope.selection).toEqual(['B']);
scope.selection = ['A']; scope.selection = ['A'];
scope.$eval(); scope.$eval();
expect(element[0].childNodes[0].selected).toEqual(true); 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(){ it('should ignore text widget which have no name', function(){