mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-04-25 00:54:46 +00:00
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:
parent
934f44f69e
commit
347be5ae9a
13 changed files with 433 additions and 171 deletions
|
|
@ -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) #
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
var ngdoc = require('ngdoc.js');
|
||||
var DOM = require('dom.js').DOM;
|
||||
|
||||
describe('ngdoc', function(){
|
||||
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);');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(')');
|
||||
});
|
||||
});
|
||||
|
|
@ -325,26 +290,34 @@ Doc.prototype = {
|
|||
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,6 +393,20 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
100
src/widgets.js
100
src/widgets.js
|
|
@ -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
|
||||
|
|
@ -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???
|
||||
};
|
||||
|
||||
|
|
@ -517,28 +539,36 @@ angularWidget('select', function(element){
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,4 +34,19 @@ describe("formatter", function(){
|
|||
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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -442,18 +442,78 @@ describe("widget", function(){
|
|||
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(){
|
||||
|
|
|
|||
Loading…
Reference in a new issue