input[type=text] now works with binding, validation, formatter, required

This commit is contained in:
Misko Hevery 2010-03-24 16:13:42 -07:00
parent 3d36942400
commit 0c42eb9909
7 changed files with 190 additions and 60 deletions

View file

@ -44,6 +44,8 @@ function extensionList(angular, name) {
} }
var consoleNode, msie, var consoleNode, msie,
VALUE = 'value',
NOOP = 'noop',
jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy
slice = Array.prototype.slice, slice = Array.prototype.slice,
angular = window['angular'] || (window['angular'] = {}), angular = window['angular'] || (window['angular'] = {}),
@ -92,6 +94,9 @@ function isObject(value){ return typeof value == 'object';}
function isString(value){ return typeof value == 'string';} function isString(value){ return typeof value == 'string';}
function isArray(value) { return value instanceof Array; } function isArray(value) { return value instanceof Array; }
function isFunction(value){ return typeof value == 'function';} function isFunction(value){ return typeof value == 'function';}
function lowercase(value){ return isString(value) ? value.toLowerCase() : value; }
function uppercase(value){ return isString(value) ? value.toUpperCase() : value; }
function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; };
function log(a, b, c){ function log(a, b, c){
var console = window['console']; var console = window['console'];
@ -244,10 +249,6 @@ function outerHTML(node) {
return outerHTML; return outerHTML;
} }
function trim(str) {
return str.replace(/^ */, '').replace(/ *$/, '');
}
function toBoolean(value) { function toBoolean(value) {
var v = ("" + value).toLowerCase(); var v = ("" + value).toLowerCase();
if (v == 'f' || v == '0' || v == 'false' || v == 'no') if (v == 'f' || v == '0' || v == 'false' || v == 'no')

View file

@ -105,9 +105,7 @@ Compiler.prototype = {
templatize: function(element){ templatize: function(element){
var self = this, var self = this,
elementName = element[0].nodeName, widget = self.widgets[element[0].nodeName],
widgets = self.widgets,
widget = widgets[elementName],
directives = self.directives, directives = self.directives,
descend = true, descend = true,
exclusive = false, exclusive = false,

View file

@ -1,4 +1,6 @@
foreach({ foreach({
'noop': noop,
'regexp': function(value, regexp, msg) { 'regexp': function(value, regexp, msg) {
if (!value.match(regexp)) { if (!value.match(regexp)) {
return msg || return msg ||
@ -7,7 +9,7 @@ foreach({
return null; return null;
} }
}, },
'number': function(value, min, max) { 'number': function(value, min, max) {
var num = 1 * value; var num = 1 * value;
if (num == value) { if (num == value) {
@ -19,40 +21,40 @@ foreach({
} }
return null; return null;
} else { } else {
return "Value is not a number."; return "Not a number";
} }
}, },
'integer': function(value, min, max) { 'integer': function(value, min, max) {
var numberError = angularValidator['number'](value, min, max); var numberError = angularValidator['number'](value, min, max);
if (numberError) return numberError; if (numberError) return numberError;
if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) { if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) {
return "Value is not a whole number."; return "Not a whole number";
} }
return null; return null;
}, },
'date': function(value, min, max) { 'date': function(value, min, max) {
if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) { if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) {
return null; return null;
} }
return "Value is not a date. (Expecting format: 12/31/2009)."; return "Value is not a date. (Expecting format: 12/31/2009).";
}, },
'ssn': function(value) { 'ssn': function(value) {
if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) { if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) {
return null; return null;
} }
return "SSN needs to be in 999-99-9999 format."; return "SSN needs to be in 999-99-9999 format.";
}, },
'email': function(value) { 'email': function(value) {
if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) { if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) {
return null; return null;
} }
return "Email needs to be in username@host.com format."; return "Email needs to be in username@host.com format.";
}, },
'phone': function(value) { 'phone': function(value) {
if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) { if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) {
return null; return null;
@ -62,14 +64,14 @@ foreach({
} }
return "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly."; return "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly.";
}, },
'url': function(value) { 'url': function(value) {
if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) { if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) {
return null; return null;
} }
return "URL needs to be in http://server[:port]/path format."; return "URL needs to be in http://server[:port]/path format.";
}, },
'json': function(value) { 'json': function(value) {
try { try {
fromJson(value); fromJson(value);
@ -78,7 +80,7 @@ foreach({
return e.toString(); return e.toString();
} }
}, },
'asynchronous': function(text, asynchronousFn) { 'asynchronous': function(text, asynchronousFn) {
var stateKey = '$validateState'; var stateKey = '$validateState';
var lastKey = '$lastKey'; var lastKey = '$lastKey';

View file

@ -77,22 +77,24 @@ JQLite.prototype = {
}, },
bind: function(type, fn){ bind: function(type, fn){
var element = this[0], var self = this,
bind = this.data('bind'), element = self[0],
bind = self.data('bind'),
eventHandler; eventHandler;
if (!bind) this.data('bind', bind = {}); if (!bind) this.data('bind', bind = {});
eventHandler = bind[type]; foreach(type.split(' '), function(type){
if (!eventHandler) { eventHandler = bind[type];
bind[type] = eventHandler = function() { if (!eventHandler) {
var self = this; bind[type] = eventHandler = function() {
foreach(eventHandler.fns, function(fn){ foreach(eventHandler.fns, function(fn){
fn.apply(self, arguments); fn.apply(self, arguments);
}); });
}; };
eventHandler.fns = []; eventHandler.fns = [];
addEventListener(element, type, eventHandler); addEventListener(element, type, eventHandler);
} }
eventHandler.fns.push(fn); eventHandler.fns.push(fn);
});
}, },
trigger: function(type) { trigger: function(type) {
@ -134,6 +136,10 @@ JQLite.prototype = {
return false; return false;
}, },
removeClass: function(selector) {
this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", ""));
},
addClass: function( selector ) { addClass: function( selector ) {
if (!this.hasClass(selector)) { if (!this.hasClass(selector)) {
this[0].className += ' ' + selector; this[0].className += ' ' + selector;

View file

@ -1,12 +1,79 @@
///////////////////////////////////////// function scopeAccessor(scope, element) {
///////////////////////////////////////// var expr = element.attr('name'),
///////////////////////////////////////// farmatterName = element.attr('ng-format') || NOOP,
///////////////////////////////////////// formatter = angularFormatter(farmatterName);
///////////////////////////////////////// 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));
},
set: function(value) {
scope.$eval(expr + '=' + toJson(formatter['parse'](value)));
}
};
}
function domAccessor(element) {
var validatorName = element.attr('ng-validate') || NOOP,
validator = angularValidator(validatorName),
required = element.attr('ng-required'),
lastError;
required = required || required == '';
if (!validator) throw "Validator named '" + validatorName + "' not found.";
function validate(value) {
var error = required && !trim(value) ? "Required" : validator(value);
if (error !== lastError) {
if (error) {
element.addClass(NG_VALIDATION_ERROR);
element.attr(NG_ERROR, error);
} else {
element.removeClass(NG_VALIDATION_ERROR);
element.removeAttr(NG_ERROR);
}
lastError = error;
}
return value;
}
return {
get: function(){
return validate(element.attr(VALUE));
},
set: function(value){
element.attr(VALUE, validate(value));
}
};
}
var NG_ERROR = 'ng-error',
NG_VALIDATION_ERROR = 'ng-validation-error',
INPUT_META = {
'text': ["", 'keyup change']
};
angularWidget('INPUT', function input(element){
var meta = INPUT_META[lowercase(element.attr('type'))];
return meta ? function(element) {
var scope = scopeAccessor(this, element),
dom = domAccessor(element);
scope.set(dom.get() || meta[0]);
element.bind(meta[1], function(){
scope.set(dom.get());
});
this.$watch(scope.get, dom.set);
} : 0;
});
/////////////////////////////////////////
/////////////////////////////////////////
/////////////////////////////////////////
/////////////////////////////////////////
/////////////////////////////////////////
//widget related //widget related
//ng-validate, ng-required, ng-formatter //ng-validate, ng-required, ng-formatter

View file

@ -26,7 +26,7 @@ ValidatorTest.prototype.testRegexp = function() {
}; };
ValidatorTest.prototype.testNumber = function() { ValidatorTest.prototype.testNumber = function() {
assertEquals(angular.validator.number("ab"), "Value is not a number."); assertEquals(angular.validator.number("ab"), "Not a number");
assertEquals(angular.validator.number("-0.1",0), "Value can not be less than 0."); assertEquals(angular.validator.number("-0.1",0), "Value can not be less than 0.");
assertEquals(angular.validator.number("10.1",0,10), "Value can not be greater than 10."); assertEquals(angular.validator.number("10.1",0,10), "Value can not be greater than 10.");
assertEquals(angular.validator.number("1.2"), null); assertEquals(angular.validator.number("1.2"), null);
@ -34,10 +34,10 @@ ValidatorTest.prototype.testNumber = function() {
}; };
ValidatorTest.prototype.testInteger = function() { ValidatorTest.prototype.testInteger = function() {
assertEquals(angular.validator.integer("ab"), "Value is not a number."); assertEquals(angular.validator.integer("ab"), "Not a number");
assertEquals(angular.validator.integer("1.1"), "Value is not a whole number."); assertEquals(angular.validator.integer("1.1"), "Not a whole number");
assertEquals(angular.validator.integer("1.0"), "Value is not a whole number."); assertEquals(angular.validator.integer("1.0"), "Not a whole number");
assertEquals(angular.validator.integer("1."), "Value is not a whole number."); assertEquals(angular.validator.integer("1."), "Not a whole number");
assertEquals(angular.validator.integer("-1",0), "Value can not be less than 0."); assertEquals(angular.validator.integer("-1",0), "Value can not be less than 0.");
assertEquals(angular.validator.integer("11",0,10), "Value can not be greater than 10."); assertEquals(angular.validator.integer("11",0,10), "Value can not be greater than 10.");
assertEquals(angular.validator.integer("1"), null); assertEquals(angular.validator.integer("1"), null);
@ -86,7 +86,7 @@ describe('Validator:asynchronous', function(){
var asynchronous = angular.validator.asynchronous; var asynchronous = angular.validator.asynchronous;
var self; var self;
var value, fn; var value, fn;
beforeEach(function(){ beforeEach(function(){
value = null; value = null;
fn = null; fn = null;
@ -96,10 +96,10 @@ describe('Validator:asynchronous', function(){
$updateView: noop $updateView: noop
}; };
}); });
it('should make a request and show spinner', function(){ it('should make a request and show spinner', function(){
var x = compile('<input name="name" ng-validate="asynchronous:asyncFn"/>') var x = compile('<input name="name" ng-validate="asynchronous:asyncFn"/>');
var asyncFn = function(v,f){value=v; fn=f}; var asyncFn = function(v,f){value=v; fn=f;};
var input = x.node.find(":input"); var input = x.node.find(":input");
x.scope.set("asyncFn", asyncFn); x.scope.set("asyncFn", asyncFn);
x.scope.set("name", "misko"); x.scope.set("name", "misko");
@ -110,29 +110,29 @@ describe('Validator:asynchronous', function(){
expect(input.hasClass('ng-input-indicator-wait')).toBeFalsy(); expect(input.hasClass('ng-input-indicator-wait')).toBeFalsy();
expect(input.attr('ng-error')).toEqual("myError"); expect(input.attr('ng-error')).toEqual("myError");
}); });
it("should not make second request to same value", function(){ it("should not make second request to same value", function(){
asynchronous.call(self, "kai", function(v,f){value=v; fn=f;}); asynchronous.call(self, "kai", function(v,f){value=v; fn=f;});
expect(value).toEqual('kai'); expect(value).toEqual('kai');
expect(self.$invalidWidgets).toEqual([self.$element]); expect(self.$invalidWidgets).toEqual([self.$element]);
var spy = jasmine.createSpy(); var spy = jasmine.createSpy();
asynchronous.call(self, "kai", spy); asynchronous.call(self, "kai", spy);
expect(spy).wasNotCalled(); expect(spy).wasNotCalled();
asynchronous.call(self, "misko", spy); asynchronous.call(self, "misko", spy);
expect(spy).wasCalled(); expect(spy).wasCalled();
}); });
it("should ignore old callbacks, and not remove spinner", function(){ it("should ignore old callbacks, and not remove spinner", function(){
var firstCb, secondCb; var firstCb, secondCb;
asynchronous.call(self, "first", function(v,f){value=v; firstCb=f;}); asynchronous.call(self, "first", function(v,f){value=v; firstCb=f;});
asynchronous.call(self, "second", function(v,f){value=v; secondCb=f;}); asynchronous.call(self, "second", function(v,f){value=v; secondCb=f;});
firstCb(); firstCb();
expect($(self.$element).hasClass('ng-input-indicator-wait')).toBeTruthy(); expect($(self.$element).hasClass('ng-input-indicator-wait')).toBeTruthy();
secondCb(); secondCb();
expect($(self.$element).hasClass('ng-input-indicator-wait')).toBeFalsy(); expect($(self.$element).hasClass('ng-input-indicator-wait')).toBeFalsy();
}); });
}); });

View file

@ -1,4 +1,4 @@
describe("widgets", function(){ describe("input widget", function(){
var compile, element, scope; var compile, element, scope;
@ -15,14 +15,70 @@ describe("widgets", function(){
}); });
afterEach(function(){ afterEach(function(){
if (element) { if (element) element.remove();
element.remove();
}
expect(_(jqCache).size()).toEqual(0); expect(_(jqCache).size()).toEqual(0);
}); });
it('should fail', function(){ it('should input-text auto init and handle keyup/change events', function(){
fail('iueoi'); compile('<input type="Text" name="name" value="Misko"/>');
expect(scope.get('name')).toEqual("Misko");
scope.set('name', 'Adam');
scope.updateView();
expect(element.attr('value')).toEqual("Adam");
element.attr('value', 'Shyam');
element.trigger('keyup');
expect(scope.get('name')).toEqual('Shyam');
element.attr('value', 'Kai');
element.trigger('change');
expect(scope.get('name')).toEqual('Kai');
});
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.updateView();
expect(element.attr('value')).toEqual("x, y, z");
element.attr('value', '1, 2, 3');
element.trigger('keyup');
expect(scope.get('list')).toEqual(['1', '2', '3']);
});
it("should process ng-validation", function(){
compile('<input type="text" name="price" value="abc" ng-validate="number"/>');
expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-error')).toEqual('Not a number');
scope.set('price', '123');
scope.updateView();
expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-error')).toBeFalsy();
element.attr('value', 'x');
element.trigger('keyup');
expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-error')).toEqual('Not a number');
});
it("should process ng-required", function(){
compile('<input type="text" name="price" ng-required/>');
expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-error')).toEqual('Required');
scope.set('price', 'xxx');
scope.updateView();
expect(element.hasClass('ng-validation-error')).toBeFalsy();
expect(element.attr('ng-error')).toBeFalsy();
element.attr('value', '');
element.trigger('keyup');
expect(element.hasClass('ng-validation-error')).toBeTruthy();
expect(element.attr('ng-error')).toEqual('Required');
}); });
}); });