mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 23:40:23 +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) #
|
# <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) #
|
||||||
|
|
|
||||||
|
|
@ -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);');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
|
||||||
100
src/widgets.js
100
src/widgets.js
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(){
|
||||||
|
|
|
||||||
|
|
@ -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(){
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue