Merc
Merc

Reputation: 17057

ValidationTextBox in dojo using Ajax vor validation

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:

  1. 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!

  2. It seems like a lot of work for something that everything ought to be ready to go

  3. (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

Answers (2)

Merc
Merc

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

mschr
mschr

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

Related Questions