diff options
| -rw-r--r-- | src/Angular.js | 9 | ||||
| -rw-r--r-- | src/Compiler.js | 4 | ||||
| -rw-r--r-- | src/Validators.js | 24 | ||||
| -rw-r--r-- | src/jqLite.js | 34 | ||||
| -rw-r--r-- | src/widgets2.js | 71 | ||||
| -rw-r--r-- | test/ValidatorsTest.js | 34 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 68 |
7 files changed, 187 insertions, 57 deletions
diff --git a/src/Angular.js b/src/Angular.js index 95f7325a..b76926b9 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -44,6 +44,8 @@ function extensionList(angular, name) { } var consoleNode, msie, + VALUE = 'value', + NOOP = 'noop', jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy slice = Array.prototype.slice, angular = window['angular'] || (window['angular'] = {}), @@ -92,6 +94,9 @@ function isObject(value){ return typeof value == 'object';} function isString(value){ return typeof value == 'string';} function isArray(value) { return value instanceof Array; } 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){ var console = window['console']; @@ -244,10 +249,6 @@ function outerHTML(node) { return outerHTML; } -function trim(str) { - return str.replace(/^ */, '').replace(/ *$/, ''); -} - function toBoolean(value) { var v = ("" + value).toLowerCase(); if (v == 'f' || v == '0' || v == 'false' || v == 'no') diff --git a/src/Compiler.js b/src/Compiler.js index ba598a43..4423fcef 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -105,9 +105,7 @@ Compiler.prototype = { templatize: function(element){ var self = this, - elementName = element[0].nodeName, - widgets = self.widgets, - widget = widgets[elementName], + widget = self.widgets[element[0].nodeName], directives = self.directives, descend = true, exclusive = false, diff --git a/src/Validators.js b/src/Validators.js index b7efcb4a..cdff5e1a 100644 --- a/src/Validators.js +++ b/src/Validators.js @@ -1,4 +1,6 @@ foreach({ + 'noop': noop, + 'regexp': function(value, regexp, msg) { if (!value.match(regexp)) { return msg || @@ -7,7 +9,7 @@ foreach({ return null; } }, - + 'number': function(value, min, max) { var num = 1 * value; if (num == value) { @@ -19,40 +21,40 @@ foreach({ } return null; } else { - return "Value is not a number."; + return "Not a number"; } }, - + 'integer': function(value, min, max) { var numberError = angularValidator['number'](value, min, max); if (numberError) return numberError; if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) { - return "Value is not a whole number."; + return "Not a whole number"; } return null; }, - + 'date': function(value, min, max) { if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) { return null; } return "Value is not a date. (Expecting format: 12/31/2009)."; }, - + 'ssn': function(value) { if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) { return null; } return "SSN needs to be in 999-99-9999 format."; }, - + 'email': function(value) { if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) { return null; } return "Email needs to be in username@host.com format."; }, - + 'phone': function(value) { if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) { 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."; }, - + 'url': function(value) { if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) { return null; } return "URL needs to be in http://server[:port]/path format."; }, - + 'json': function(value) { try { fromJson(value); @@ -78,7 +80,7 @@ foreach({ return e.toString(); } }, - + 'asynchronous': function(text, asynchronousFn) { var stateKey = '$validateState'; var lastKey = '$lastKey'; diff --git a/src/jqLite.js b/src/jqLite.js index 035a7a1b..2ac3f6c9 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -77,22 +77,24 @@ JQLite.prototype = { }, bind: function(type, fn){ - var element = this[0], - bind = this.data('bind'), + var self = this, + element = self[0], + bind = self.data('bind'), eventHandler; if (!bind) this.data('bind', bind = {}); - eventHandler = bind[type]; - if (!eventHandler) { - bind[type] = eventHandler = function() { - var self = this; - foreach(eventHandler.fns, function(fn){ - fn.apply(self, arguments); - }); - }; - eventHandler.fns = []; - addEventListener(element, type, eventHandler); - } - eventHandler.fns.push(fn); + foreach(type.split(' '), function(type){ + eventHandler = bind[type]; + if (!eventHandler) { + bind[type] = eventHandler = function() { + foreach(eventHandler.fns, function(fn){ + fn.apply(self, arguments); + }); + }; + eventHandler.fns = []; + addEventListener(element, type, eventHandler); + } + eventHandler.fns.push(fn); + }); }, trigger: function(type) { @@ -134,6 +136,10 @@ JQLite.prototype = { return false; }, + removeClass: function(selector) { + this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", "")); + }, + addClass: function( selector ) { if (!this.hasClass(selector)) { this[0].className += ' ' + selector; diff --git a/src/widgets2.js b/src/widgets2.js index b0f467d4..cee0e49e 100644 --- a/src/widgets2.js +++ b/src/widgets2.js @@ -1,3 +1,72 @@ +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; +}); + + + + ///////////////////////////////////////// ///////////////////////////////////////// ///////////////////////////////////////// @@ -6,8 +75,6 @@ - - //widget related //ng-validate, ng-required, ng-formatter //ng-error diff --git a/test/ValidatorsTest.js b/test/ValidatorsTest.js index 5449ebb0..6b61c273 100644 --- a/test/ValidatorsTest.js +++ b/test/ValidatorsTest.js @@ -26,7 +26,7 @@ ValidatorTest.prototype.testRegexp = 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("10.1",0,10), "Value can not be greater than 10."); assertEquals(angular.validator.number("1.2"), null); @@ -34,10 +34,10 @@ ValidatorTest.prototype.testNumber = function() { }; ValidatorTest.prototype.testInteger = function() { - assertEquals(angular.validator.integer("ab"), "Value is not a number."); - assertEquals(angular.validator.integer("1.1"), "Value is not a whole number."); - assertEquals(angular.validator.integer("1.0"), "Value is not a whole number."); - assertEquals(angular.validator.integer("1."), "Value is not a whole number."); + assertEquals(angular.validator.integer("ab"), "Not a number"); + assertEquals(angular.validator.integer("1.1"), "Not a whole number"); + assertEquals(angular.validator.integer("1.0"), "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("11",0,10), "Value can not be greater than 10."); assertEquals(angular.validator.integer("1"), null); @@ -86,7 +86,7 @@ describe('Validator:asynchronous', function(){ var asynchronous = angular.validator.asynchronous; var self; var value, fn; - + beforeEach(function(){ value = null; fn = null; @@ -96,10 +96,10 @@ describe('Validator:asynchronous', function(){ $updateView: noop }; }); - + it('should make a request and show spinner', function(){ - var x = compile('<input name="name" ng-validate="asynchronous:asyncFn"/>') - var asyncFn = function(v,f){value=v; fn=f}; + var x = compile('<input name="name" ng-validate="asynchronous:asyncFn"/>'); + var asyncFn = function(v,f){value=v; fn=f;}; var input = x.node.find(":input"); x.scope.set("asyncFn", asyncFn); x.scope.set("name", "misko"); @@ -110,29 +110,29 @@ describe('Validator:asynchronous', function(){ expect(input.hasClass('ng-input-indicator-wait')).toBeFalsy(); expect(input.attr('ng-error')).toEqual("myError"); }); - + it("should not make second request to same value", function(){ asynchronous.call(self, "kai", function(v,f){value=v; fn=f;}); expect(value).toEqual('kai'); expect(self.$invalidWidgets).toEqual([self.$element]); - + var spy = jasmine.createSpy(); asynchronous.call(self, "kai", spy); expect(spy).wasNotCalled(); - + asynchronous.call(self, "misko", spy); - expect(spy).wasCalled(); + expect(spy).wasCalled(); }); - + it("should ignore old callbacks, and not remove spinner", function(){ var firstCb, secondCb; asynchronous.call(self, "first", function(v,f){value=v; firstCb=f;}); asynchronous.call(self, "second", function(v,f){value=v; secondCb=f;}); - + firstCb(); expect($(self.$element).hasClass('ng-input-indicator-wait')).toBeTruthy(); - + secondCb(); expect($(self.$element).hasClass('ng-input-indicator-wait')).toBeFalsy(); }); -});
\ No newline at end of file +}); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 3b6be8ec..da9d5f9b 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -1,4 +1,4 @@ -describe("widgets", function(){ +describe("input widget", function(){ var compile, element, scope; @@ -15,14 +15,70 @@ describe("widgets", function(){ }); afterEach(function(){ - if (element) { - element.remove(); - } + if (element) element.remove(); expect(_(jqCache).size()).toEqual(0); }); - it('should fail', function(){ - fail('iueoi'); + it('should input-text auto init and handle keyup/change events', function(){ + 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'); }); }); |
