Reputation: 17057
Here is another issue I cannot quite believe hasn't been resolved yet... I am creating a registration form. Now... I need to do some ajax async validation. This is what I am doing right now (see code at the bottom).
It's great, it caches already checked values, it's async, etc. but:
You are never 10000% sure it's validated before submission. This is a huge deal, as I ALSO check things on the server side. But still... making this code work "forcing" validation on submission turns it into a real monster!
It seems like a lot of work for something that everything ought to be ready to go
(Could be related to 2) I cannot think of a way to make this actually generic. I have another 10 fields which will need similar validation, but all validations differ a little, except the Ajax calls... and I am not cutting and pasting this code 10 times!
Am I missing something really obvious? Any ideas?
define([
'dojo/_base/declare',
'dijit/form/ValidationTextBox',
'dojo/_base/lang',
'app/globals',
], function(
declare
, ValidationTextBox
, lang
, g
){
return declare('app.ValidationWorkspace', [ ValidationTextBox ], {
ajaxSaidNo: {},
ajaxSaidYes: {},
ajaxRequested: {},
onChange: function(value){
this.validate();
},
validator: function(value){
// console.log("Started validation for " + value);
// Ajax has already said no -- returning false straight away
if(this.ajaxSaidNo[value] ){
this.invalidMessage = 'Workspace taken';
return false;
}
// Run the normal field validators -- if they fail,
// return false
var validation = Validators.workspace(value);
if( ! validation.result){
this.invalidMessage = validation.message;
return false;
// At this point, regexp validators passed and Ajax hasn't said "no".
} else {
// console.log("OK, ajasSaidYes for " + value + " is " + this.ajaxSaidYes[value]);
if(! this.ajaxSaidYes[value] ){
g.stores.workspacesAnon.query({name: value}).then(
lang.hitch(this, function(res){
if(res && res.length ){
this.ajaxSaidNo[value] = true;
//console.log("Added to Ajaxfailed: " + value);
this.validate();
} else {
//console.log("Added to Ajaxsuccess: " + value);
this.ajaxSaidYes[value] = true;
this.validate();
}
})
);
}
return true;
}
},
invalidMessage: "Workspace name not valid",
missingMessage: "Workspace name cannot be empty",
});
}
);
Upvotes: 0
Views: 1537
Reputation: 17057
The accepted answer might be the right solution. However, for non-blocking ajax checks, I ended up writing a simple mixin:
define([
'dojo/_base/declare',
'dojo/_base/lang',
'app/globals',
], function(
declare
,lang
, g
){
return declare(null, {
ajaxSaidNo: {},
ajaxSaidYes: {},
ajaxRequested: {},
constructor: function(){
// Declaring object variable in constructor to make sure that
// they are not class-wide (since they will be in the prototype)
this.ajaxSaidNo = {};
this.ajaxSaidYes = {};
this.ajaxRequested = {};
},
// Overloads the validator, adding extra stuff
ajaxValidate: function(value, options){
// Set some defaults
options.ajaxInvalidMessage = options.ajaxInvalidMessage || "Value not allowed";
options.ajaxStore = options.ajaxStore || null;
options.ajaxFilterField = options.ajaxFilterField || 'name';
// No ajaxStore query available, return true
if( ! options.ajaxStore ){
return true;
}
// console.log("Started validation for " + value);
// Ajax has already said no -- returning false straight away
if(this.ajaxSaidNo[value] ){
this.invalidMessage = options.ajaxInvalidMessage;
return false;
}
// console.log("OK, ajasSaidYes for " + value + " is " + this.ajaxSaidYes[value]);
if(! this.ajaxSaidYes[value] ){
var filterObject = {};
filterObject[options.ajaxFilterField] = value;
options.ajaxStore.query( filterObject ).then(
lang.hitch(this, function(res){
if(res && res.length ){
this.ajaxSaidNo[value] = true;
//console.log("Added to Ajaxfailed: " + value);
this.validate();
} else {
//console.log("Added to Ajaxsuccess: " + value);
this.ajaxSaidYes[value] = true;
this.validate();
}
})
);
}
return true;
}
});
}
);
Then in the validate function of a textbox (or any field, really), just add the bare minimum to make it work:
return this.ajaxValidate(value, {
ajaxInvalidMessage: "Workspace taken",
ajaxStore: g.stores.workspacesAnon,
ajaxFilterField: 'name',
});
That's it... nice and clear, and you can apply it to any field you like. (If I receive enough encouraging comments on my approach, I will make this one as the "accepted" answer...)
Merc.
Upvotes: 0
Reputation: 8641
Abstract yourself from the TextBox itself and customize the form onSubmit instead. The general callback used before formDom.submit() simply calls formDijit.validate() and passes that boolean to form.submit (if its false, no submission takes place of course).
In other words, it expects the validators to run blocking/atomically and to not return before each validator function has resolved the return value.
In your situation, the dojo.Deferred
comes in handy. Find yourself picking up responses from the ajax-requests during form.onSubmit - and once all that has 'in-flight' ajax calls have resolved, then if true fire submission.
<div tada-dojo-type="dijit.form.Form" id="form">
<div data-dojo-type="app.ValidationWorkspace" name="inputfield" id="inputfield1"></div>
</div>
As example to fill in missing stuff I cant see in your codebase, the below follows this simplification
<script>
declare('app.ValidationWorkspace', [ ValidationTextBox ], {
invalidMessageLocal : 'JS renders your entry invalid',
invalidMessageServer: 'Server expects something different then you entered',
invalidMessage: '',
regExp: 'Foo*', // validates false if does not start with "Foo"
dfdValidator: undefined,
validator: function() {
var curVal = this.get("value");
if(new RegExp(this.regExp).test(curVal)) {
// client-side passes, run serverside
this.dfdValidator = dojo.xhrGet({
url: 'validate.sapi'+
'?field=' + this.inputNode.name +
'&value='+ value
});
// we cannot truly base anything form-wise here, but we might want
// to set a message (and show it, left out)
this.dfdValidator.then(dojo.hitch(this, function(response) {
this.invalidMessage = this.invalidMessageServer + " : " + response;
}));
}
}
});
</script>
And mutate the forms onSubmit with something similar to this:
<script>
dojo.addOnLoad(function() {
dijit.byId('form').onSubmit = function() {
var immediate = true;
var deferreds = [];
dojo.some(dijit.byId('form')._getDescendantFormWidgets(), function(w) {
if(w.validate) {
var tmp = w.validate();
if(!tmp) immediate = false;
else { // check if it has servervalidation in flight
if(w.dfdValidator) {
// it does, store it so we can keep a count and
// not submit before all has passed
deferreds.push(w.dfdValidator);
}
}
}
return tmp; /* if false - one field failed - foreach(some) stops loop */
}); // end dojo.some
var form = this.domNode
var submit = function() {
// logic to submit your values here
store.put(dojo.formToJson(form));
}
// simply stop here if local validators fail
if(immediate == false) {
return false;
} else if(deferreds.length > 0) {
// do not act before all is done
var dfdCounter = new dojo.Deferred();
dfdCounter.then(submit);
var dfdCount = deferreds.length;
var incrCount = 0;
var delayed = true; // delayed validation
dojo.forEach(deferred, function(serverValidatedComplete) {
serverValidatedComplete.then(function(serverresponse) {
incrCount++;
if(/REGEXPTOTESTRESPONSEFALSE/.test(serverresponse))
delayed = false;
if(dfdCount == incrCount && delayed == true)
{ /* YES, all is done, any errors? */
dfdCounter.resolve(/* optional arguments here */);
}
});
});
} else {
// no server validators are running, local validation succeeds
submit();
}
// never return true - or <form> will submit as HTML should be doing
return false;
} // end mutation
});
</script>
Upvotes: 1