aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMisko Hevery2010-05-10 20:24:20 -0700
committerMisko Hevery2010-05-10 20:24:20 -0700
commit5dda723185a9037b7e92828d32430c21838ee216 (patch)
tree15e12ff20e020bb524d704bc7e0d2f8d1f6f1b40
parentf5027cc375cf29d8a78679297d9f6bdca9567eb7 (diff)
downloadangular.js-5dda723185a9037b7e92828d32430c21838ee216.tar.bz2
improved handling of text fields when formater fails to prevent clobering of field
-rw-r--r--lib/jasmine-jstd-adapter/JasmineAdapter.js12
-rw-r--r--scenario/widgets.html5
-rw-r--r--src/formatters.js23
-rw-r--r--src/widgets.js78
-rw-r--r--test/testabilityPatch.js25
-rw-r--r--test/widgetsSpec.js169
6 files changed, 208 insertions, 104 deletions
diff --git a/lib/jasmine-jstd-adapter/JasmineAdapter.js b/lib/jasmine-jstd-adapter/JasmineAdapter.js
index ba54251a..0fdc4612 100644
--- a/lib/jasmine-jstd-adapter/JasmineAdapter.js
+++ b/lib/jasmine-jstd-adapter/JasmineAdapter.js
@@ -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);
diff --git a/scenario/widgets.html b/scenario/widgets.html
index 61badf1c..242fd9e6 100644
--- a/scenario/widgets.html
+++ b/scenario/widgets.html
@@ -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>
diff --git a/src/formatters.js b/src/formatters.js
index ee63c1a5..40462cf3 100644
--- a/src/formatters.js
+++ b/src/formatters.js
@@ -1,11 +1,20 @@
-function formater(format, parse) {return {'format':format, 'parse':parse || format};}
-function toString(obj) {return isDefined(obj) ? "" + obj : obj;}
+function formatter(format, parse) {return {'format':format, 'parse':parse || format};}
+function toString(obj) {return (isDefined(obj) && obj !== null) ? "" + obj : obj;}
+
+var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
+
extend(angularFormatter, {
- 'noop':formater(identity, identity),
- 'boolean':formater(toString, toBoolean),
- 'number':formater(toString, function(obj){return 1*obj;}),
+ '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':formater(
+ '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) : ""; }
)
});
diff --git a/src/widgets.js b/src/widgets.js
index 064b27fe..48898a9a 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -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);
+ }
};
}
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index 4d129f60..d621b1f1 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -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)));
}
diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js
index c9665f1e..b365175d 100644
--- a/test/widgetsSpec.js
+++ b/test/widgetsSpec.js
@@ -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);
-
- 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('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;
+ 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");
+
+ 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);
});
- 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');
-
- scope.$set('price', '123');
- scope.$eval();
- expect(element.hasClass('ng-validation-error')).toBeFalsy();
- expect(element.attr('ng-validation-error')).toBeFalsy();
+ describe("ng-format", function(){
+
+ 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']);
+
+ 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 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);
+ });
+
+ 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);
+ });
- 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;
+ 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(){