mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +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
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,7 @@
|
|||
# <angular/> 0.9.10 flea-whisperer (in-progress) #
|
||||
|
||||
### Bug Fixes
|
||||
- html select (one/multiple) could not chose from a list of objects, since DOM requires string ids.
|
||||
|
||||
|
||||
# <angular/> 0.9.9 time-shift (2011-01-13) #
|
||||
|
|
@ -99,9 +101,9 @@
|
|||
- small docs improvements (mainly docs for the $resource service)
|
||||
|
||||
### Breaking changes
|
||||
- Angular expressions in the view used to support regular expressions. This feature was rarely
|
||||
used and added unnecessary complexity. It not a good idea to have regexps in the view anyway,
|
||||
so we removed this support. If you had any regexp in your views, you will have to move them to
|
||||
- Angular expressions in the view used to support regular expressions. This feature was rarely
|
||||
used and added unnecessary complexity. It not a good idea to have regexps in the view anyway,
|
||||
so we removed this support. If you had any regexp in your views, you will have to move them to
|
||||
your controllers. (commit e5e69d9b90850eb653883f52c76e28dd870ee067)
|
||||
|
||||
|
||||
|
|
@ -120,7 +122,7 @@
|
|||
- docs app UI polishing with dual scrolling and other improvements
|
||||
|
||||
### Bug Fixes
|
||||
- `select` widget now behaves correctly when it's `option` items are created via `ng:repeat`
|
||||
- `select` widget now behaves correctly when it's `option` items are created via `ng:repeat`
|
||||
(issue #170)
|
||||
- fix for async xhr cache issue #152 by adding `$browser.defer` and `$defer` service
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
var ngdoc = require('ngdoc.js');
|
||||
var DOM = require('dom.js').DOM;
|
||||
|
||||
describe('ngdoc', function(){
|
||||
var Doc = ngdoc.Doc;
|
||||
|
|
@ -253,5 +254,67 @@ describe('ngdoc', function(){
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('usage', function(){
|
||||
var dom;
|
||||
|
||||
beforeEach(function(){
|
||||
dom = new DOM();
|
||||
this.addMatchers({
|
||||
toContain: function(text) {
|
||||
this.actual = this.actual.toString();
|
||||
return this.actual.indexOf(text) > -1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter', function(){
|
||||
it('should format', function(){
|
||||
var doc = new Doc({
|
||||
ngdoc:'formatter',
|
||||
shortName:'myFilter',
|
||||
param: [
|
||||
{name:'a'},
|
||||
{name:'b'}
|
||||
]
|
||||
});
|
||||
doc.html_usage_filter(dom);
|
||||
expect(dom).toContain('myFilter_expression | myFilter:b');
|
||||
expect(dom).toContain('angular.filter.myFilter(a, b)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validator', function(){
|
||||
it('should format', function(){
|
||||
var doc = new Doc({
|
||||
ngdoc:'validator',
|
||||
shortName:'myValidator',
|
||||
param: [
|
||||
{name:'a'},
|
||||
{name:'b'}
|
||||
]
|
||||
});
|
||||
doc.html_usage_validator(dom);
|
||||
expect(dom).toContain('ng:validate="myValidator:b"');
|
||||
expect(dom).toContain('angular.validator.myValidator(a, b)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatter', function(){
|
||||
it('should format', function(){
|
||||
var doc = new Doc({
|
||||
ngdoc:'formatter',
|
||||
shortName:'myFormatter',
|
||||
param: [
|
||||
{name:'a'},
|
||||
]
|
||||
});
|
||||
doc.html_usage_formatter(dom);
|
||||
expect(dom).toContain('ng:format="myFormatter:a"');
|
||||
expect(dom).toContain('var userInputString = angular.formatter.myFormatter.format(modelValue, a);');
|
||||
expect(dom).toContain('var modelValue = angular.formatter.myFormatter.parse(userInputString, a);');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -231,15 +231,7 @@ Doc.prototype = {
|
|||
dom.code(function(){
|
||||
dom.text(self.name);
|
||||
dom.text('(');
|
||||
var first = true;
|
||||
(self.param || []).forEach(function(param){
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
dom.text(', ');
|
||||
}
|
||||
dom.text(param.name);
|
||||
});
|
||||
self.parameters(dom, ', ');
|
||||
dom.text(');');
|
||||
});
|
||||
|
||||
|
|
@ -273,44 +265,17 @@ Doc.prototype = {
|
|||
dom.text(self.shortName);
|
||||
dom.text('_expression | ');
|
||||
dom.text(self.shortName);
|
||||
var first = true;
|
||||
(self.param||[]).forEach(function(param){
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
if (param.optional) {
|
||||
dom.tag('i', function(){
|
||||
dom.text('[:' + param.name + ']');
|
||||
});
|
||||
} else {
|
||||
dom.text(':' + param.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.parameters(dom, ':', true);
|
||||
dom.text(' }}');
|
||||
});
|
||||
});
|
||||
|
||||
dom.h3('In JavaScript', function(){
|
||||
dom.h('In JavaScript', function(){
|
||||
dom.tag('code', function(){
|
||||
dom.text('angular.filter.');
|
||||
dom.text(self.shortName);
|
||||
dom.text('(');
|
||||
var first = true;
|
||||
(self.param||[]).forEach(function(param){
|
||||
if (first) {
|
||||
first = false;
|
||||
dom.text(param.name);
|
||||
} else {
|
||||
if (param.optional) {
|
||||
dom.tag('i', function(){
|
||||
dom.text('[, ' + param.name + ']');
|
||||
});
|
||||
} else {
|
||||
dom.text(', ' + param.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.parameters(dom, ', ');
|
||||
dom.text(')');
|
||||
});
|
||||
});
|
||||
|
|
@ -319,32 +284,40 @@ Doc.prototype = {
|
|||
self.html_usage_returns(dom);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
html_usage_formatter: function(dom){
|
||||
var self = this;
|
||||
dom.h('Usage', function(){
|
||||
dom.h('In HTML Template Binding', function(){
|
||||
dom.code(function(){
|
||||
dom.text('<input type="text" ng:format="');
|
||||
if (self.inputType=='select')
|
||||
dom.text('<select name="bindExpression"');
|
||||
else
|
||||
dom.text('<input type="text" name="bindExpression"');
|
||||
dom.text(' ng:format="');
|
||||
dom.text(self.shortName);
|
||||
self.parameters(dom, ':', false, true);
|
||||
dom.text('">');
|
||||
});
|
||||
});
|
||||
|
||||
dom.h3('In JavaScript', function(){
|
||||
dom.h('In JavaScript', function(){
|
||||
dom.code(function(){
|
||||
dom.text('var userInputString = angular.formatter.');
|
||||
dom.text(self.shortName);
|
||||
dom.text('.format(modelValue);');
|
||||
});
|
||||
dom.html('<br/>');
|
||||
dom.code(function(){
|
||||
dom.text('.format(modelValue');
|
||||
self.parameters(dom, ', ', false, true);
|
||||
dom.text(');');
|
||||
dom.text('\n');
|
||||
dom.text('var modelValue = angular.formatter.');
|
||||
dom.text(self.shortName);
|
||||
dom.text('.parse(userInputString);');
|
||||
dom.text('.parse(userInputString');
|
||||
self.parameters(dom, ', ', false, true);
|
||||
dom.text(');');
|
||||
});
|
||||
});
|
||||
|
||||
self.html_usage_parameters(dom);
|
||||
self.html_usage_returns(dom);
|
||||
});
|
||||
},
|
||||
|
|
@ -356,18 +329,7 @@ Doc.prototype = {
|
|||
dom.code(function(){
|
||||
dom.text('<input type="text" ng:validate="');
|
||||
dom.text(self.shortName);
|
||||
var first = true;
|
||||
(self.param||[]).forEach(function(param){
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
if (param.optional) {
|
||||
dom.text('[:' + param.name + ']');
|
||||
} else {
|
||||
dom.text(':' + param.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.parameters(dom, ':', true);
|
||||
dom.text('"/>');
|
||||
});
|
||||
});
|
||||
|
|
@ -377,19 +339,7 @@ Doc.prototype = {
|
|||
dom.text('angular.validator.');
|
||||
dom.text(self.shortName);
|
||||
dom.text('(');
|
||||
var first = true;
|
||||
(self.param||[]).forEach(function(param){
|
||||
if (first) {
|
||||
first = false;
|
||||
dom.text(param.name);
|
||||
} else {
|
||||
if (param.optional) {
|
||||
dom.text('[, ' + param.name + ']');
|
||||
} else {
|
||||
dom.text(', ' + param.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.parameters(dom, ', ');
|
||||
dom.text(')');
|
||||
});
|
||||
});
|
||||
|
|
@ -443,8 +393,22 @@ Doc.prototype = {
|
|||
},
|
||||
|
||||
html_usage_service: function(dom){
|
||||
}
|
||||
},
|
||||
|
||||
parameters: function(dom, separator, skipFirst, prefix) {
|
||||
var sep = prefix ? separator : '';
|
||||
(this.param||[]).forEach(function(param, i){
|
||||
if (!(skipFirst && i==0)) {
|
||||
if (param.optional) {
|
||||
dom.text('[' + sep + param.name + ']');
|
||||
} else {
|
||||
dom.text(sep + param.name);
|
||||
}
|
||||
}
|
||||
sep = separator;
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
148
src/widgets.js
148
src/widgets.js
|
|
@ -8,16 +8,16 @@
|
|||
* standard HTML set. These widgets are bound using the name attribute
|
||||
* to an expression. In addition they can have `ng:validate`, `ng:required`,
|
||||
* `ng:format`, `ng:change` attribute to further control their behavior.
|
||||
*
|
||||
*
|
||||
* @usageContent
|
||||
* see example below for usage
|
||||
*
|
||||
*
|
||||
* <input type="text|checkbox|..." ... />
|
||||
* <textarea ... />
|
||||
* <select ...>
|
||||
* <option>...</option>
|
||||
* </select>
|
||||
*
|
||||
*
|
||||
* @example
|
||||
<table style="font-size:.9em;">
|
||||
<tr>
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
<td><tt>{{input6|json}}</tt></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
* @scenario
|
||||
* it('should exercise text', function(){
|
||||
* input('input1').enter('Carlos');
|
||||
|
|
@ -134,14 +134,19 @@
|
|||
|
||||
function modelAccessor(scope, element) {
|
||||
var expr = element.attr('name');
|
||||
var assign;
|
||||
if (expr) {
|
||||
assign = parser(expr).assignable().assign;
|
||||
if (!assign) throw new Error("Expression '" + expr + "' is not assignable.");
|
||||
return {
|
||||
get: function() {
|
||||
return scope.$eval(expr);
|
||||
},
|
||||
set: function(value) {
|
||||
if (value !== _undefined) {
|
||||
return scope.$tryEval(expr + '=' + toJson(value), element);
|
||||
return scope.$tryEval(function(){
|
||||
assign(scope, value);
|
||||
}, element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -151,15 +156,14 @@ function modelAccessor(scope, element) {
|
|||
function modelFormattedAccessor(scope, element) {
|
||||
var accessor = modelAccessor(scope, element),
|
||||
formatterName = element.attr('ng:format') || NOOP,
|
||||
formatter = angularFormatter(formatterName);
|
||||
if (!formatter) throw "Formatter named '" + formatterName + "' not found.";
|
||||
formatter = compileFormatter(formatterName);
|
||||
if (accessor) {
|
||||
return {
|
||||
get: function() {
|
||||
return formatter.format(accessor.get());
|
||||
return formatter.format(scope, accessor.get());
|
||||
},
|
||||
set: function(value) {
|
||||
return accessor.set(formatter.parse(value));
|
||||
return accessor.set(formatter.parse(scope, value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -169,6 +173,10 @@ function compileValidator(expr) {
|
|||
return parser(expr).validator()();
|
||||
}
|
||||
|
||||
function compileFormatter(expr) {
|
||||
return parser(expr).formatter()();
|
||||
}
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc widget
|
||||
|
|
@ -195,7 +203,7 @@ function compileValidator(expr) {
|
|||
|
||||
I need an integer or nothing:
|
||||
<input type="text" name="value" ng:validate="integer"><br/>
|
||||
*
|
||||
*
|
||||
* @scenario
|
||||
it('should check ng:validate', function(){
|
||||
expect(element('.doc-example-live :input:last').attr('className')).
|
||||
|
|
@ -214,7 +222,7 @@ function compileValidator(expr) {
|
|||
* @description
|
||||
* The `ng:required` attribute widget validates that the user input is present. It is a special case
|
||||
* of the {@link angular.widget.@ng:validate ng:validate} attribute widget.
|
||||
*
|
||||
*
|
||||
* @element INPUT
|
||||
* @css ng-validation-error
|
||||
*
|
||||
|
|
@ -253,10 +261,10 @@ function compileValidator(expr) {
|
|||
* array.
|
||||
*
|
||||
* @example
|
||||
Enter a comma separated list of items:
|
||||
Enter a comma separated list of items:
|
||||
<input type="text" name="list" ng:format="list" value="table, chairs, plate">
|
||||
<pre>list={{list}}</pre>
|
||||
*
|
||||
*
|
||||
* @scenario
|
||||
it('should check ng:format', function(){
|
||||
expect(binding('list')).toBe('list=["table","chairs","plate"]');
|
||||
|
|
@ -269,11 +277,10 @@ function valueAccessor(scope, element) {
|
|||
validator = compileValidator(validatorName),
|
||||
requiredExpr = element.attr('ng:required'),
|
||||
formatterName = element.attr('ng:format') || NOOP,
|
||||
formatter = angularFormatter(formatterName),
|
||||
formatter = compileFormatter(formatterName),
|
||||
format, parse, lastError, required,
|
||||
invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop};
|
||||
if (!validator) throw "Validator named '" + validatorName + "' not found.";
|
||||
if (!formatter) throw "Formatter named '" + formatterName + "' not found.";
|
||||
format = formatter.format;
|
||||
parse = formatter.parse;
|
||||
if (requiredExpr) {
|
||||
|
|
@ -291,7 +298,7 @@ function valueAccessor(scope, element) {
|
|||
if (lastError)
|
||||
elementError(element, NG_VALIDATION_ERROR, _null);
|
||||
try {
|
||||
var value = parse(element.val());
|
||||
var value = parse(scope, element.val());
|
||||
validate();
|
||||
return value;
|
||||
} catch (e) {
|
||||
|
|
@ -301,7 +308,7 @@ function valueAccessor(scope, element) {
|
|||
},
|
||||
set: function(value) {
|
||||
var oldValue = element.val(),
|
||||
newValue = format(value);
|
||||
newValue = format(scope, value);
|
||||
if (oldValue != newValue) {
|
||||
element.val(newValue || ''); // needed for ie
|
||||
}
|
||||
|
|
@ -355,19 +362,22 @@ function radioAccessor(scope, element) {
|
|||
}
|
||||
|
||||
function optionsAccessor(scope, element) {
|
||||
var options = element[0].options;
|
||||
var formatterName = element.attr('ng:format') || NOOP,
|
||||
formatter = compileFormatter(formatterName);
|
||||
return {
|
||||
get: function(){
|
||||
var values = [];
|
||||
forEach(options, function(option){
|
||||
if (option.selected) values.push(option.value);
|
||||
forEach(element[0].options, function(option){
|
||||
if (option.selected) values.push(formatter.parse(scope, option.value));
|
||||
});
|
||||
return values;
|
||||
},
|
||||
set: function(values){
|
||||
var keys = {};
|
||||
forEach(values, function(value){ keys[value] = true; });
|
||||
forEach(options, function(option){
|
||||
forEach(values, function(value){
|
||||
keys[formatter.format(scope, value)] = true;
|
||||
});
|
||||
forEach(element[0].options, function(option){
|
||||
option.selected = keys[option.value];
|
||||
});
|
||||
}
|
||||
|
|
@ -376,6 +386,18 @@ function optionsAccessor(scope, element) {
|
|||
|
||||
function noopAccessor() { return { get: noop, set: noop }; }
|
||||
|
||||
/*
|
||||
* TODO: refactor
|
||||
*
|
||||
* The table bellow is not quite right. In some cases the formatter is on the model side
|
||||
* and in some cases it is on the view side. This is a historical artifact
|
||||
*
|
||||
* The concept of model/view accessor is useful for anyone who is trying to develop UI, and
|
||||
* so it should be exposed to others. There should be a form object which keeps track of the
|
||||
* accessors and also acts as their factory. It should expose it as an object and allow
|
||||
* the validator to publish errors to it, so that the the error messages can be bound to it.
|
||||
*
|
||||
*/
|
||||
var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true),
|
||||
buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop),
|
||||
INPUT_TYPE = {
|
||||
|
|
@ -389,8 +411,8 @@ var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, ini
|
|||
'image': buttonWidget,
|
||||
'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)),
|
||||
'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit),
|
||||
'select-one': inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(_null)),
|
||||
'select-multiple': inputWidget('change', modelFormattedAccessor, optionsAccessor, initWidgetValue([]))
|
||||
'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(_null)),
|
||||
'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([]))
|
||||
// 'file': fileWidget???
|
||||
};
|
||||
|
||||
|
|
@ -427,7 +449,7 @@ function radioInit(model, view, element) {
|
|||
*
|
||||
* @description
|
||||
* The directive executes an expression whenever the input widget changes.
|
||||
*
|
||||
*
|
||||
* @element INPUT
|
||||
* @param {expression} expression to execute.
|
||||
*
|
||||
|
|
@ -438,17 +460,17 @@ function radioInit(model, view, element) {
|
|||
changeCount {{textCount}}<br/>
|
||||
<input type="checkbox" name="checkbox" ng:change="checkboxCount = 1 + checkboxCount">
|
||||
changeCount {{checkboxCount}}<br/>
|
||||
*
|
||||
*
|
||||
* @scenario
|
||||
it('should check ng:change', function(){
|
||||
expect(binding('textCount')).toBe('0');
|
||||
expect(binding('checkboxCount')).toBe('0');
|
||||
|
||||
|
||||
using('.doc-example-live').input('text').enter('abc');
|
||||
expect(binding('textCount')).toBe('1');
|
||||
expect(binding('checkboxCount')).toBe('0');
|
||||
|
||||
|
||||
|
||||
|
||||
using('.doc-example-live').input('checkbox').check();
|
||||
expect(binding('textCount')).toBe('1');
|
||||
expect(binding('checkboxCount')).toBe('1');
|
||||
|
|
@ -504,41 +526,49 @@ angularWidget('select', function(element){
|
|||
* <select name="selection">
|
||||
* <option ng:repeat="x in [1,2]">{{x}}</option>
|
||||
* </select>
|
||||
*
|
||||
*
|
||||
* The issue is that the select gets evaluated before option is unrolled.
|
||||
* This means that the selection is undefined, but the browser
|
||||
* default behavior is to show the top selection in the list.
|
||||
* default behavior is to show the top selection in the list.
|
||||
* To fix that we register a $update function on the select element
|
||||
* and the option creation then calls the $update function when it is
|
||||
* unrolled. The $update function then calls this update function, which
|
||||
* and the option creation then calls the $update function when it is
|
||||
* unrolled. The $update function then calls this update function, which
|
||||
* then tries to determine if the model is unassigned, and if so it tries to
|
||||
* chose one of the options from the list.
|
||||
*/
|
||||
angularWidget('option', function(){
|
||||
this.descend(true);
|
||||
this.directives(true);
|
||||
return function(element) {
|
||||
var select = element.parent();
|
||||
return function(option) {
|
||||
var select = option.parent();
|
||||
var isMultiple = select.attr('multiple') == '';
|
||||
var scope = retrieveScope(select);
|
||||
var model = modelFormattedAccessor(scope, select);
|
||||
var view = valueAccessor(scope, select);
|
||||
var option = element;
|
||||
var model = modelAccessor(scope, select);
|
||||
var formattedModel = modelFormattedAccessor(scope, select);
|
||||
var view = isMultiple
|
||||
? optionsAccessor(scope, select)
|
||||
: valueAccessor(scope, select);
|
||||
var lastValue = option.attr($value);
|
||||
var lastSelected = option.attr('ng-' + $selected);
|
||||
element.data($$update, function(){
|
||||
var value = option.attr($value);
|
||||
var selected = option.attr('ng-' + $selected);
|
||||
var modelValue = model.get();
|
||||
if (lastSelected != selected || lastValue != value) {
|
||||
lastSelected = selected;
|
||||
lastValue = value;
|
||||
if (selected || modelValue == _null || modelValue == _undefined)
|
||||
model.set(value);
|
||||
if (value == modelValue) {
|
||||
view.set(lastValue);
|
||||
var wasSelected = option.attr('ng-' + $selected);
|
||||
option.data($$update, isMultiple
|
||||
? function(){
|
||||
view.set(model.get());
|
||||
}
|
||||
}
|
||||
});
|
||||
: function(){
|
||||
var currentValue = option.attr($value);
|
||||
var isSelected = option.attr('ng-' + $selected);
|
||||
var modelValue = model.get();
|
||||
if (wasSelected != isSelected || lastValue != currentValue) {
|
||||
wasSelected = isSelected;
|
||||
lastValue = currentValue;
|
||||
if (isSelected || !modelValue == null || modelValue == undefined )
|
||||
formattedModel.set(currentValue);
|
||||
if (currentValue == modelValue) {
|
||||
view.set(lastValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -549,12 +579,12 @@ angularWidget('option', function(){
|
|||
*
|
||||
* @description
|
||||
* Include external HTML fragment.
|
||||
*
|
||||
* Keep in mind that Same Origin Policy applies to included resources
|
||||
*
|
||||
* Keep in mind that Same Origin Policy applies to included resources
|
||||
* (e.g. ng:include won't work for file:// access).
|
||||
*
|
||||
* @param {string} src expression evaluating to URL.
|
||||
* @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an
|
||||
* @param {Scope=} [scope=new_child_scope] optional expression which evaluates to an
|
||||
* instance of angular.scope to set the HTML fragment to.
|
||||
* @param {string=} onload Expression to evaluate when a new partial is loaded.
|
||||
*
|
||||
|
|
@ -636,17 +666,17 @@ angularWidget('ng:include', function(element){
|
|||
*
|
||||
* @description
|
||||
* Conditionally change the DOM structure.
|
||||
*
|
||||
*
|
||||
* @usageContent
|
||||
* <any ng:switch-when="matchValue1">...</any>
|
||||
* <any ng:switch-when="matchValue2">...</any>
|
||||
* ...
|
||||
* <any ng:switch-default>...</any>
|
||||
*
|
||||
*
|
||||
* @param {*} on expression to match against <tt>ng:switch-when</tt>.
|
||||
* @paramDescription
|
||||
* @paramDescription
|
||||
* On child elments add:
|
||||
*
|
||||
*
|
||||
* * `ng:switch-when`: the case statement to match against. If match then this
|
||||
* case will be displayed.
|
||||
* * `ng:switch-default`: the default case when no other casses match.
|
||||
|
|
|
|||
|
|
@ -33,5 +33,20 @@ describe("formatter", function(){
|
|||
assertEquals('a', angular.formatter.trim.format(" a "));
|
||||
assertEquals('a', angular.formatter.trim.parse(' a '));
|
||||
});
|
||||
|
||||
describe('json', function(){
|
||||
it('should treat empty string as null', function(){
|
||||
expect(angular.formatter.json.parse('')).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('index', function(){
|
||||
it('should parse an object from array', function(){
|
||||
expect(angular.formatter.index.parse('1', ['A', 'B', 'C'])).toEqual('B');
|
||||
});
|
||||
it('should format an index from array', function(){
|
||||
expect(angular.formatter.index.format('B', ['A', 'B', 'C'])).toEqual('1');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,11 +92,11 @@ describe('json', function(){
|
|||
it('should not serialize undefined values', function() {
|
||||
expect(angular.toJson({A:undefined})).toEqual('{}');
|
||||
});
|
||||
|
||||
|
||||
it('should not serialize $window object', function() {
|
||||
expect(toJson(window)).toEqual('WINDOW');
|
||||
});
|
||||
|
||||
|
||||
it('should not serialize $document object', function() {
|
||||
expect(toJson(document)).toEqual('DOCUMENT');
|
||||
});
|
||||
|
|
@ -116,6 +116,13 @@ describe('json', function(){
|
|||
expect(fromJson("{exp:1.2e-10}")).toEqual({exp:1.2E-10});
|
||||
});
|
||||
|
||||
it('should ignore non-strings', function(){
|
||||
expect(fromJson([])).toEqual([]);
|
||||
expect(fromJson({})).toEqual({});
|
||||
expect(fromJson(null)).toEqual(null);
|
||||
expect(fromJson(undefined)).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
//run these tests only in browsers that have native JSON parser
|
||||
if (JSON && JSON.parse) {
|
||||
|
|
@ -187,18 +194,18 @@ describe('json', function(){
|
|||
expect(function(){fromJson('[].constructor');}).
|
||||
toThrow(new Error("Parse Error: Token '.' is not valid json at column 3 of expression [[].constructor] starting at [.constructor]."));
|
||||
});
|
||||
|
||||
|
||||
it('should not allow object dereference', function(){
|
||||
expect(function(){fromJson('{a:1, b: $location, c:1}');}).toThrow();
|
||||
expect(function(){fromJson("{a:1, b:[1]['__parent__']['location'], c:1}");}).toThrow();
|
||||
});
|
||||
|
||||
|
||||
it('should not allow assignments', function(){
|
||||
expect(function(){fromJson("{a:1, b:[1]=1, c:1}");}).toThrow();
|
||||
expect(function(){fromJson("{a:1, b:=1, c:1}");}).toThrow();
|
||||
expect(function(){fromJson("{a:1, b:x=1, c:1}");}).toThrow();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ describe("widget", function(){
|
|||
expect(scope.$get('name')).toEqual('Kai');
|
||||
expect(scope.$get('count')).toEqual(2);
|
||||
});
|
||||
|
||||
|
||||
it('should not trigger eval if value does not change', function(){
|
||||
compile('<input type="Text" name="name" value="Misko" ng:change="count = count + 1" ng:init="count=0"/>');
|
||||
expect(scope.name).toEqual("Misko");
|
||||
|
|
@ -53,7 +53,7 @@ describe("widget", function(){
|
|||
expect(scope.name).toEqual("Misko");
|
||||
expect(scope.count).toEqual(0);
|
||||
});
|
||||
|
||||
|
||||
it('should allow complex refernce binding', function(){
|
||||
compile('<div ng:init="obj={abc:{}}">'+
|
||||
'<input type="Text" name="obj[\'abc\'].name" value="Misko""/>'+
|
||||
|
|
@ -416,7 +416,7 @@ describe("widget", function(){
|
|||
scope.$eval();
|
||||
expect(element[0].childNodes[1].selected).toEqual(true);
|
||||
});
|
||||
|
||||
|
||||
it('should select default option on repeater', function(){
|
||||
compile(
|
||||
'<select name="selection">' +
|
||||
|
|
@ -424,7 +424,7 @@ describe("widget", function(){
|
|||
'</select>');
|
||||
expect(scope.selection).toEqual('1');
|
||||
});
|
||||
|
||||
|
||||
it('should select selected option on repeater', function(){
|
||||
compile(
|
||||
'<select name="selection">' +
|
||||
|
|
@ -433,7 +433,7 @@ describe("widget", function(){
|
|||
'</select>');
|
||||
expect(scope.selection).toEqual('ABC');
|
||||
});
|
||||
|
||||
|
||||
it('should select dynamically selected option on repeater', function(){
|
||||
compile(
|
||||
'<select name="selection">' +
|
||||
|
|
@ -441,21 +441,81 @@ describe("widget", function(){
|
|||
'</select>');
|
||||
expect(scope.selection).toEqual('2');
|
||||
});
|
||||
|
||||
|
||||
it('should allow binding to objects through JSON', function(){
|
||||
compile(
|
||||
'<select name="selection" ng:format="json">' +
|
||||
'<option ng:repeat="obj in objs" value="{{obj}}">{{obj.name}}</option>' +
|
||||
'</select>');
|
||||
scope.objs = [{name:'A'}, {name:'B'}];
|
||||
scope.$eval();
|
||||
expect(scope.selection).toEqual({name:'A'});
|
||||
});
|
||||
|
||||
it('should allow binding to objects through index', function(){
|
||||
compile(
|
||||
'<select name="selection" ng:format="index:objs">' +
|
||||
'<option ng:repeat="obj in objs" value="{{$index}}">{{obj.name}}</option>' +
|
||||
'</select>');
|
||||
scope.objs = [{name:'A'}, {name:'B'}];
|
||||
scope.$eval();
|
||||
expect(scope.selection).toBe(scope.objs[0]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should support type="select-multiple"', function(){
|
||||
compile(
|
||||
'<select name="selection" multiple>' +
|
||||
'<option>A</option>' +
|
||||
'<option selected>B</option>' +
|
||||
'</select>');
|
||||
expect(scope.selection).toEqual(['B']);
|
||||
scope.selection = ['A'];
|
||||
scope.$eval();
|
||||
expect(element[0].childNodes[0].selected).toEqual(true);
|
||||
describe('select-multiple', function(){
|
||||
it('should support type="select-multiple"', function(){
|
||||
compile('<select name="selection" multiple>' +
|
||||
'<option>A</option>' +
|
||||
'<option selected>B</option>' +
|
||||
'</select>');
|
||||
expect(scope.selection).toEqual(['B']);
|
||||
scope.selection = ['A'];
|
||||
scope.$eval();
|
||||
expect(element[0].childNodes[0].selected).toEqual(true);
|
||||
});
|
||||
|
||||
it('should allow binding to objects through index', function(){
|
||||
compile('<select name="selection" multiple ng:format="index:list">' +
|
||||
'<option selected value="0">A</option>' +
|
||||
'<option selected value="1">B</option>' +
|
||||
'<option value="2">C</option>' +
|
||||
'</select>',
|
||||
function(){
|
||||
scope.list = [{name:'A'}, {name:'B'}, {name:'C'}];
|
||||
});
|
||||
scope.$eval();
|
||||
expect(scope.selection).toEqual([{name:'A'}, {name:'B'}]);
|
||||
});
|
||||
|
||||
it('should be empty array when no items are selected', function(){
|
||||
compile(
|
||||
'<select name="selection" multiple ng:format="index:list">' +
|
||||
'<option value="0">A</option>' +
|
||||
'<option value="1">B</option>' +
|
||||
'<option value="2">C</option>' +
|
||||
'</select>');
|
||||
scope.list = [{name:'A'}, {name:'B'}, {name:'C'}];
|
||||
scope.$eval();
|
||||
expect(scope.selection).toEqual([]);
|
||||
});
|
||||
|
||||
it('should be contain the selected object', function(){
|
||||
compile('<select name="selection" multiple ng:format="index:list">' +
|
||||
'<option value="0">A</option>' +
|
||||
'<option value="1" selected>B</option>' +
|
||||
'<option value="2">C</option>' +
|
||||
'</select>',
|
||||
function(){
|
||||
scope.list = [{name:'A'}, {name:'B'}, {name:'C'}];
|
||||
});
|
||||
scope.$eval();
|
||||
expect(scope.selection).toEqual([{name:'B'}]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should ignore text widget which have no name', function(){
|
||||
compile('<input type="text"/>');
|
||||
expect(scope.$element.attr('ng-exception')).toBeFalsy();
|
||||
|
|
@ -504,7 +564,7 @@ describe("widget", function(){
|
|||
scope.$eval();
|
||||
expect(element.text()).toEqual('true:misko');
|
||||
});
|
||||
|
||||
|
||||
it("should compare stringified versions", function(){
|
||||
var switchWidget = angular.widget('ng:switch');
|
||||
expect(switchWidget.equals(true, 'true')).toEqual(true);
|
||||
|
|
@ -521,7 +581,7 @@ describe("widget", function(){
|
|||
scope.$eval();
|
||||
expect(element.text()).toEqual('one');
|
||||
});
|
||||
|
||||
|
||||
it("should match urls", function(){
|
||||
var scope = angular.compile('<ng:switch on="url" using="route:params"><div ng:switch-when="/Book/:name">{{params.name}}</div></ng:switch>');
|
||||
scope.url = '/Book/Moby';
|
||||
|
|
|
|||
Loading…
Reference in a new issue