mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
improved handling of text fields when formater fails to prevent clobering of field
This commit is contained in:
parent
f5027cc375
commit
5dda723185
6 changed files with 203 additions and 99 deletions
|
|
@ -9,7 +9,7 @@
|
|||
function bind(_this, _function){
|
||||
return function(){
|
||||
return _function.call(_this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var currentFrame = frame(null, null);
|
||||
|
|
@ -49,14 +49,22 @@
|
|||
|
||||
})(jasmine.Env.prototype.describe);
|
||||
|
||||
var id = 0;
|
||||
|
||||
jasmine.Env.prototype.it = (function(it){
|
||||
return function(desc, itFn){
|
||||
var self = this;
|
||||
var spec = it.apply(this, arguments);
|
||||
var currentSpec = this.currentSpec;
|
||||
if (!currentSpec.$id) {
|
||||
currentSpec.$id = id++;
|
||||
}
|
||||
var frame = this.jstdFrame = currentFrame;
|
||||
this.jstdFrame.testCase.prototype['test that it ' + desc] = function(){
|
||||
var name = 'test that it ' + desc;
|
||||
if (this.jstdFrame.testCase.prototype[name])
|
||||
throw "Spec with name '" + desc + "' already exists.";
|
||||
this.jstdFrame.testCase.prototype[name] = function(){
|
||||
jasmine.getEnv().currentSpec = currentSpec;
|
||||
frame.runBefore.apply(currentSpec);
|
||||
try {
|
||||
itFn.apply(currentSpec);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@
|
|||
<tr><th colspan="3">Input text field</th></tr>
|
||||
<tr>
|
||||
<td>basic</td>
|
||||
<td><input type="text" name="text.basic" ng-required ng-validate="number" ng-format="number"/></td>
|
||||
<td>
|
||||
<input type="text" name="text.basic" ng-required ng-validate="number" ng-format="number"/>
|
||||
<input type="text" name="text.basic" ng-format="number"/>
|
||||
</td>
|
||||
<td>text.basic={{text.basic}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
function formater(format, parse) {return {'format':format, 'parse':parse || format};}
|
||||
function toString(obj) {return isDefined(obj) ? "" + obj : obj;}
|
||||
extend(angularFormatter, {
|
||||
'noop':formater(identity, identity),
|
||||
'boolean':formater(toString, toBoolean),
|
||||
'number':formater(toString, function(obj){return 1*obj;}),
|
||||
function formatter(format, parse) {return {'format':format, 'parse':parse || format};}
|
||||
function toString(obj) {return (isDefined(obj) && obj !== null) ? "" + obj : obj;}
|
||||
|
||||
'list':formater(
|
||||
var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
|
||||
|
||||
extend(angularFormatter, {
|
||||
'noop':formatter(identity, identity),
|
||||
'boolean':formatter(toString, toBoolean),
|
||||
'number':formatter(toString,
|
||||
function(obj){
|
||||
if (isString(obj) && NUMBER.exec(obj)) {
|
||||
return obj ? 1*obj : null;
|
||||
}
|
||||
throw "Not a number";
|
||||
}),
|
||||
|
||||
'list':formatter(
|
||||
function(obj) { return obj ? obj.join(", ") : obj; },
|
||||
function(value) {
|
||||
var list = [];
|
||||
|
|
@ -17,7 +26,7 @@ extend(angularFormatter, {
|
|||
}
|
||||
),
|
||||
|
||||
'trim':formater(
|
||||
'trim':formatter(
|
||||
function(obj) { return obj ? trim("" + obj) : ""; }
|
||||
)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
function modelAccessor(scope, element) {
|
||||
var expr = element.attr('name'),
|
||||
farmatterName = element.attr('ng-format') || NOOP,
|
||||
formatter = angularFormatter(farmatterName);
|
||||
var expr = element.attr('name');
|
||||
if (!expr) throw "Required field 'name' not found.";
|
||||
if (!formatter) throw "Formatter named '" + farmatterName + "' not found.";
|
||||
return {
|
||||
get: function() {
|
||||
return formatter['format'](scope.$eval(expr));
|
||||
return scope.$eval(expr);
|
||||
},
|
||||
set: function(value) {
|
||||
scope.$tryEval(expr + '=' + toJson(formatter['parse'](value)), element);
|
||||
if (value !== undefined) {
|
||||
scope.$tryEval(expr + '=' + toJson(value), element);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -22,27 +21,49 @@ function valueAccessor(scope, element) {
|
|||
var validatorName = element.attr('ng-validate') || NOOP,
|
||||
validator = compileValidator(validatorName),
|
||||
required = element.attr('ng-required'),
|
||||
lastError,
|
||||
farmatterName = element.attr('ng-format') || NOOP,
|
||||
formatter = angularFormatter(farmatterName),
|
||||
format, parse, lastError;
|
||||
invalidWidgets = scope.$invalidWidgets || {markValid:noop, markInvalid:noop};
|
||||
required = required || required === '';
|
||||
if (!validator) throw "Validator named '" + validatorName + "' not found.";
|
||||
function validate(value) {
|
||||
var force = false;
|
||||
if (isUndefined(value)) {
|
||||
value = element.val();
|
||||
force = true;
|
||||
if (!formatter) throw "Formatter named '" + farmatterName + "' not found.";
|
||||
format = formatter.format;
|
||||
parse = formatter.parse;
|
||||
required = required || required === '';
|
||||
|
||||
element.data('$validate', validate);
|
||||
return {
|
||||
get: function(){
|
||||
if (lastError)
|
||||
elementError(element, NG_VALIDATION_ERROR, null);
|
||||
try {
|
||||
return parse(element.val());
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
elementError(element, NG_VALIDATION_ERROR, e);
|
||||
}
|
||||
},
|
||||
set: function(value) {
|
||||
var oldValue = element.val(),
|
||||
newValue = format(value);
|
||||
if (oldValue != newValue) {
|
||||
element.val(newValue);
|
||||
}
|
||||
validate();
|
||||
}
|
||||
};
|
||||
|
||||
function validate() {
|
||||
var value = trim(element.val());
|
||||
if (element[0].disabled || element[0].readOnly) {
|
||||
elementError(element, NG_VALIDATION_ERROR, null);
|
||||
invalidWidgets.markValid(element);
|
||||
return value;
|
||||
}
|
||||
var error,
|
||||
validateScope = extend(new (extend(function(){}, {prototype:scope}))(), {$element:element});
|
||||
error = required && !trim(value) ?
|
||||
"Required" :
|
||||
(trim(value) ? validator({state:validateScope, scope:{get:validateScope.$get, set:validateScope.$set}}, value) : null);
|
||||
if (error !== lastError || force) {
|
||||
} else {
|
||||
var error,
|
||||
validateScope = extend(new (extend(function(){}, {prototype:scope}))(), {$element:element});
|
||||
error = required && !value ?
|
||||
"Required" :
|
||||
(value ? validator({state:validateScope, scope:{get:validateScope.$get, set:validateScope.$set}}, value) : null);
|
||||
elementError(element, NG_VALIDATION_ERROR, error);
|
||||
lastError = error;
|
||||
if (error) {
|
||||
|
|
@ -51,13 +72,7 @@ function valueAccessor(scope, element) {
|
|||
invalidWidgets.markValid(element);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
element.data('$validate', validate);
|
||||
return {
|
||||
get: function(){ return validate(element.val()); },
|
||||
set: function(value){ element.val(validate(value)); }
|
||||
};
|
||||
}
|
||||
|
||||
function checkedAccessor(scope, element) {
|
||||
|
|
@ -106,7 +121,7 @@ function optionsAccessor(scope, element) {
|
|||
|
||||
function noopAccessor() { return { get: noop, set: noop }; }
|
||||
|
||||
var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue('')),
|
||||
var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue()),
|
||||
buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop),
|
||||
INPUT_TYPE = {
|
||||
'text': textWidget,
|
||||
|
|
@ -126,9 +141,12 @@ var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initW
|
|||
|
||||
function initWidgetValue(initValue) {
|
||||
return function (model, view) {
|
||||
var value = view.get() || copy(initValue);
|
||||
if (isUndefined(model.get()) && isDefined(value))
|
||||
var value = view.get();
|
||||
if (!value && isDefined(initValue))
|
||||
value = copy(initValue);
|
||||
if (isUndefined(model.get()) && isDefined(value)) {
|
||||
model.set(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,31 @@
|
|||
jstd = jstestdriver;
|
||||
dump = bind(jstd.console, jstd.console.log);
|
||||
|
||||
beforeEach(function(){
|
||||
this.addMatchers({
|
||||
toBeInvalid: function(){
|
||||
var element = jqLite(this.actual);
|
||||
var hasClass = element.hasClass('ng-validation-error');
|
||||
var validationError = element.attr('ng-validation-error');
|
||||
this.message = function(){
|
||||
if (!hasClass)
|
||||
return "Expected class 'ng-validation-error' not found.";
|
||||
return "Expected an error message, but none was found.";
|
||||
};
|
||||
return hasClass && validationError;
|
||||
},
|
||||
|
||||
toBeValid: function(){
|
||||
var element = jqLite(this.actual);
|
||||
var hasClass = element.hasClass('ng-validation-error');
|
||||
this.message = function(){
|
||||
return "Expected to not have class 'ng-validation-error' but found.";
|
||||
};
|
||||
return !hasClass;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function nakedExpect(obj) {
|
||||
return expect(angular.fromJson(angular.toJson(obj)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,74 +20,115 @@ describe("widget", function(){
|
|||
|
||||
describe("input", function(){
|
||||
|
||||
it('should input-text auto init and handle keyup/change events', function(){
|
||||
compile('<input type="Text" name="name" value="Misko" ng-change="count = count + 1" ng-init="count=0"/>');
|
||||
expect(scope.$get('name')).toEqual("Misko");
|
||||
expect(scope.$get('count')).toEqual(0);
|
||||
describe("text", function(){
|
||||
it('should input-text auto init and handle keyup/change events', function(){
|
||||
compile('<input type="Text" name="name" value="Misko" ng-change="count = count + 1" ng-init="count=0"/>');
|
||||
expect(scope.$get('name')).toEqual("Misko");
|
||||
expect(scope.$get('count')).toEqual(0);
|
||||
|
||||
scope.$set('name', 'Adam');
|
||||
scope.$eval();
|
||||
expect(element.val()).toEqual("Adam");
|
||||
scope.$set('name', 'Adam');
|
||||
scope.$eval();
|
||||
expect(element.val()).toEqual("Adam");
|
||||
|
||||
element.val('Shyam');
|
||||
element.trigger('keyup');
|
||||
expect(scope.$get('name')).toEqual('Shyam');
|
||||
expect(scope.$get('count')).toEqual(1);
|
||||
element.val('Shyam');
|
||||
element.trigger('keyup');
|
||||
expect(scope.$get('name')).toEqual('Shyam');
|
||||
expect(scope.$get('count')).toEqual(1);
|
||||
|
||||
element.val('Kai');
|
||||
element.trigger('change');
|
||||
expect(scope.$get('name')).toEqual('Kai');
|
||||
expect(scope.$get('count')).toEqual(2);
|
||||
});
|
||||
|
||||
it("should process ng-format", function(){
|
||||
compile('<input type="Text" name="list" value="a,b,c" ng-format="list"/>');
|
||||
expect(scope.$get('list')).toEqual(['a', 'b', 'c']);
|
||||
|
||||
scope.$set('list', ['x', 'y', 'z']);
|
||||
scope.$eval();
|
||||
expect(element.val()).toEqual("x, y, z");
|
||||
|
||||
element.val('1, 2, 3');
|
||||
element.trigger('keyup');
|
||||
expect(scope.$get('list')).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
it("should process ng-format for booleans", function(){
|
||||
compile('<input type="checkbox" name="name" value="true" ng-format="boolean"/>', function(){
|
||||
scope.name = false;
|
||||
element.val('Kai');
|
||||
element.trigger('change');
|
||||
expect(scope.$get('name')).toEqual('Kai');
|
||||
expect(scope.$get('count')).toEqual(2);
|
||||
});
|
||||
expect(scope.name).toEqual(false);
|
||||
expect(scope.$element[0].checked).toEqual(false);
|
||||
});
|
||||
|
||||
it("should process ng-validate", function(){
|
||||
compile('<input type="text" name="price" value="abc" ng-validate="number"/>');
|
||||
expect(element.hasClass('ng-validation-error')).toBeTruthy();
|
||||
expect(element.attr('ng-validation-error')).toEqual('Not a number');
|
||||
describe("ng-format", function(){
|
||||
|
||||
scope.$set('price', '123');
|
||||
scope.$eval();
|
||||
expect(element.hasClass('ng-validation-error')).toBeFalsy();
|
||||
expect(element.attr('ng-validation-error')).toBeFalsy();
|
||||
it("should farmat text", function(){
|
||||
compile('<input type="Text" name="list" value="a,b,c" ng-format="list"/>');
|
||||
expect(scope.$get('list')).toEqual(['a', 'b', 'c']);
|
||||
|
||||
element.val('x');
|
||||
element.trigger('keyup');
|
||||
expect(element.hasClass('ng-validation-error')).toBeTruthy();
|
||||
expect(element.attr('ng-validation-error')).toEqual('Not a number');
|
||||
});
|
||||
scope.$set('list', ['x', 'y', 'z']);
|
||||
scope.$eval();
|
||||
expect(element.val()).toEqual("x, y, z");
|
||||
|
||||
it("should not call validator if undefinde/empty", function(){
|
||||
var lastValue = "NOT_CALLED";
|
||||
angularValidator.myValidator = function(value){lastValue = value;};
|
||||
compile('<input type="text" name="url" ng-validate="myValidator"/>');
|
||||
expect(lastValue).toEqual("NOT_CALLED");
|
||||
element.val('1, 2, 3');
|
||||
element.trigger('keyup');
|
||||
expect(scope.$get('list')).toEqual(['1', '2', '3']);
|
||||
});
|
||||
|
||||
scope.url = 'http://server';
|
||||
scope.$eval();
|
||||
expect(lastValue).toEqual("http://server");
|
||||
it("should format booleans", function(){
|
||||
compile('<input type="checkbox" name="name" value="true" ng-format="boolean"/>', function(){
|
||||
scope.name = false;
|
||||
});
|
||||
expect(scope.name).toEqual(false);
|
||||
expect(scope.$element[0].checked).toEqual(false);
|
||||
});
|
||||
|
||||
delete angularValidator.myValidator;
|
||||
it("should come up blank if null", function(){
|
||||
compile('<input type="text" name="age" ng-format="number"/>', function(){
|
||||
scope.age = null;
|
||||
});
|
||||
expect(scope.age).toBeNull();
|
||||
expect(scope.$element[0].value).toEqual('');
|
||||
});
|
||||
|
||||
it("should show incorect text while number does not parse", function(){
|
||||
compile('<input type="text" name="age" ng-format="number"/>');
|
||||
scope.age = 123;
|
||||
scope.$eval();
|
||||
scope.$element.val('123X');
|
||||
scope.$element.trigger('change');
|
||||
expect(scope.$element.val()).toEqual('123X');
|
||||
expect(scope.age).toEqual(123);
|
||||
expect(scope.$element).toBeInvalid();
|
||||
});
|
||||
|
||||
it("should clober incorect text if model changes", function(){
|
||||
compile('<input type="text" name="age" ng-format="number" value="123X"/>');
|
||||
scope.age = 456;
|
||||
scope.$eval();
|
||||
expect(scope.$element.val()).toEqual('456');
|
||||
});
|
||||
|
||||
it("should come up blank when no value specifiend", function(){
|
||||
compile('<input type="text" name="age" ng-format="number"/>');
|
||||
scope.$eval();
|
||||
expect(scope.$element.val()).toEqual('');
|
||||
expect(scope.age).toEqual(null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("ng-validate", function(){
|
||||
it("should process ng-validate", function(){
|
||||
compile('<input type="text" name="price" value="abc" ng-validate="number"/>');
|
||||
expect(element.hasClass('ng-validation-error')).toBeTruthy();
|
||||
expect(element.attr('ng-validation-error')).toEqual('Not a number');
|
||||
|
||||
scope.$set('price', '123');
|
||||
scope.$eval();
|
||||
expect(element.hasClass('ng-validation-error')).toBeFalsy();
|
||||
expect(element.attr('ng-validation-error')).toBeFalsy();
|
||||
|
||||
element.val('x');
|
||||
element.trigger('keyup');
|
||||
expect(element.hasClass('ng-validation-error')).toBeTruthy();
|
||||
expect(element.attr('ng-validation-error')).toEqual('Not a number');
|
||||
});
|
||||
|
||||
it("should not call validator if undefinde/empty", function(){
|
||||
var lastValue = "NOT_CALLED";
|
||||
angularValidator.myValidator = function(value){lastValue = value;};
|
||||
compile('<input type="text" name="url" ng-validate="myValidator"/>');
|
||||
expect(lastValue).toEqual("NOT_CALLED");
|
||||
|
||||
scope.url = 'http://server';
|
||||
scope.$eval();
|
||||
expect(lastValue).toEqual("http://server");
|
||||
|
||||
delete angularValidator.myValidator;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should ignore disabled widgets", function(){
|
||||
|
|
|
|||
Loading…
Reference in a new issue