loading....
';Y.prototype={qb:function(){if(this.da===0)(this.La=this.La||this.body.append(Y.$a).find("#ng-loading")).show();this.da++},Bb:function(){this.da--;this.da===0&&this.La.hide("fold")}}})(window,document);
+url:function(a){if(a.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(a){try{L(a);return null}catch(b){return b.toString()}},asynchronous:function(a,b){var c=this.$element,d=c.$validateState=c.$validateState||{},e=d[a],f=this.$updateView;c.$lastKey=a;if(e===undefined){k(c).addClass("ng-input-indicator-wait");e=d[a]=null;b(a,function(g){e=d[a]=g?g:false;d[c.$lastKey]!==
+null&&k(c).removeClass("ng-input-indicator-wait");f()})}e===null&&this.$invalidWidgets.push(this.R);return e}},function(a,b){ga[b]=a});na.prototype={ub:function(a,b){var c,d=a.attr("type").toLowerCase(),e=a.attr("name");if(e)e=e.split(":").pop();var f="change",g=true,h=ha[a.attr("ng-format")]||ha.noop;if(d=="button"||d=="submit"||d=="reset"||d=="image"){c=new Ra(a[0],e,h);f="click";g=false}else if(d=="text"||d=="textarea"||d=="hidden"||d=="password"){c=new sa(a[0],e,h);f="keyup change"}else if(d==
+"checkbox"){c=new ta(a[0],e,h);f="click"}else if(d=="radio"){c=new wa(a[0],e,h);f="click"}else if(d=="select-one")c=new ua(a[0],e,h);else if(d=="select-multiple")c=new va(a[0],e,h);else if(d=="file")c=this.vb(a,e,h);else throw"Unknown type: "+d;a.data("controller",c);var i=b.get("$updateView");k(c.view,":input").bind(f,function(){if(c.e(b)){var j=k(c.view).attr("ng-action")||"";b.o(c,j)&&i(b)}return g});return c},vb:function(a){var b="__uploadWidget_"+this.Ub++,c=U.template(b);a.after(c);b=this.wb({data:this.fa+
+"/admin/ServerAPI.swf",width:"95",height:"20",align:"top",wmode:"transparent"},{flashvars:"uploadWidgetId="+b,allowScriptAccess:"always"},b);a.remove();a=new U(c,a[0].name,b,this.fa+"/data/"+this.ka);k(b).data("controller",a);return a}};N.flashEvent=function(a,b,c){a=x.getElementById(a);a=k(a);var d=a.data("controller");U.prototype[b].apply(d,c);_.defer(a.scope().get("$updateView"))};U.template=function(a){return k('d;--b)this.children.pop().element.remove();if(h&&h.element[0].nodeName==="OPTION")if(d=k(h.element[0].parentNode).data("controller")){d.c=undefined;d.d(a)}})}};z.va="mouseleave mouseout click dblclick keypress keyup";z.fc=function(){z.pa();var a=k(this);a.bind(z.va,z.pa);var b=a.position(),c=x.documentElement,d=(self.innerWidth||c&&c.clientWidth||
+x.body.clientWidth)-b.left;c=a.hasClass("ng-exception")?"EXCEPTION:":"Validation error...";a=a.attr("ng-error");d=d>375?"left":"right";c=k("");k("body").append(c);if(d==="left")a=b.left+this.offsetWidth+11;else{a=b.left-315;c.find(".ng-arrow-right").css({left:301})}c.css({left:a+"px",top:b.top-3+"px"});return true};z.pa=function(){k("#ng-callout").unbind(z.va,
+z.pa).remove();return true};z.prototype={bind:function(){this.F.find(".ng-validation-error,.ng-exception").live("mouseover",z.fc)}};Y.$a='loading....
';Y.prototype={qb:function(){if(this.ea===0)(this.La=this.La||this.body.append(Y.$a).find("#ng-loading")).show();this.ea++},Bb:function(){this.ea--;this.ea===0&&this.La.hide("fold")}}})(window,document);
diff --git a/angular.js b/angular.js
index 6ff1fbb8..2684315a 100644
--- a/angular.js
+++ b/angular.js
@@ -2468,11 +2468,11 @@ Parser.prototype = {
},
filter: function(){
- return this._pipeFunction(angular['filter']);
+ return this._pipeFunction(angularFilter);
},
validator: function(){
- return this._pipeFunction(angular['validator']);
+ return this._pipeFunction(angularValidator);
},
_pipeFunction: function(fnScope){
@@ -3240,7 +3240,36 @@ foreach({
} catch (e) {
return e.toString();
}
+ },
+
+ 'asynchronous': function(text, asynchronousFn) {
+ var stateKey = '$validateState';
+ var lastKey = '$lastKey';
+ var obj = this['$element'];
+ var stateCache = obj[stateKey] = obj[stateKey] || {};
+ var state = stateCache[text];
+ var updateView = this['$updateView'];
+ obj[lastKey] = text;
+ if (state === undefined) {
+ // we have never seen this before, Request it
+ jQuery(obj).addClass('ng-input-indicator-wait');
+ state = stateCache[text] = null;
+ asynchronousFn(text, function(error){
+ state = stateCache[text] = error ? error : false;
+ if (stateCache[obj[lastKey]] !== null) {
+ jQuery(obj).removeClass('ng-input-indicator-wait');
+ }
+ updateView();
+ });
+ }
+
+ if (state === null){
+ // request in flight, mark widget invalid, but don't show it to user
+ this['$invalidWidgets'].push(this.$element);
+ }
+ return state;
}
+
}, function(v,k) {angularValidator[k] = v;});
function WidgetFactory(serverUrl, database) {
this.nextUploadId = 0;
diff --git a/css/angular.css b/css/angular.css
index 80f82762..0fb10cdf 100644
--- a/css/angular.css
+++ b/css/angular.css
@@ -182,3 +182,8 @@ div.ui-widget {
background-image: url("angular_images/indicator-wait.png");
}
+.ng-input-indicator-wait {
+ background-image: url("angular_images/indicator-wait.png");
+ background-position: right;
+ background-repeat: no-repeat;
+}
diff --git a/example/widgets.html b/example/widgets.html
index 1d74d243..d3e980a1 100644
--- a/example/widgets.html
+++ b/example/widgets.html
@@ -8,6 +8,11 @@
$(document).ready(function(){
angular.compile(document).init();
});
+ function asyncValidate(value, callback){
+ var x = value.length % 2 ? null: "even";
+ //callback(x);
+ _(callback).delay(1000, x);
+ }
@@ -20,6 +25,8 @@
+
+
form={{form}}
$invalidWidgets.length={{$invalidWidgets.length}}
diff --git a/src/Parser.js b/src/Parser.js
index 3aa644ac..b59b21a7 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -349,11 +349,11 @@ Parser.prototype = {
},
filter: function(){
- return this._pipeFunction(angular['filter']);
+ return this._pipeFunction(angularFilter);
},
validator: function(){
- return this._pipeFunction(angular['validator']);
+ return this._pipeFunction(angularValidator);
},
_pipeFunction: function(fnScope){
diff --git a/src/Validators.js b/src/Validators.js
index 84681f15..b7efcb4a 100644
--- a/src/Validators.js
+++ b/src/Validators.js
@@ -77,5 +77,34 @@ foreach({
} catch (e) {
return e.toString();
}
+ },
+
+ 'asynchronous': function(text, asynchronousFn) {
+ var stateKey = '$validateState';
+ var lastKey = '$lastKey';
+ var obj = this['$element'];
+ var stateCache = obj[stateKey] = obj[stateKey] || {};
+ var state = stateCache[text];
+ var updateView = this['$updateView'];
+ obj[lastKey] = text;
+ if (state === undefined) {
+ // we have never seen this before, Request it
+ jQuery(obj).addClass('ng-input-indicator-wait');
+ state = stateCache[text] = null;
+ asynchronousFn(text, function(error){
+ state = stateCache[text] = error ? error : false;
+ if (stateCache[obj[lastKey]] !== null) {
+ jQuery(obj).removeClass('ng-input-indicator-wait');
+ }
+ updateView();
+ });
+ }
+
+ if (state === null){
+ // request in flight, mark widget invalid, but don't show it to user
+ this['$invalidWidgets'].push(this.$element);
+ }
+ return state;
}
+
}, function(v,k) {angularValidator[k] = v;});
diff --git a/test/ValidatorsTest.js b/test/ValidatorsTest.js
index a4e603fa..5449ebb0 100644
--- a/test/ValidatorsTest.js
+++ b/test/ValidatorsTest.js
@@ -81,3 +81,58 @@ ValidatorTest.prototype.testJson = function() {
assertNotNull(angular.validator.json("''X"));
assertNull(angular.validator.json("{}"));
};
+
+describe('Validator:asynchronous', function(){
+ var asynchronous = angular.validator.asynchronous;
+ var self;
+ var value, fn;
+
+ beforeEach(function(){
+ value = null;
+ fn = null;
+ self = {
+ $element:$('')[0],
+ $invalidWidgets:[],
+ $updateView: noop
+ };
+ });
+
+ it('should make a request and show spinner', function(){
+ var x = compile('')
+ 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");
+ x.binder.updateView();
+ expect(value).toEqual('misko');
+ expect(input.hasClass('ng-input-indicator-wait')).toBeTruthy();
+ fn("myError");
+ 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();
+ });
+
+ 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
--
cgit v1.2.3