Wednesday, July 15, 2015

JQuery Validation .valid() Lies....

I had the opportunity to develop an account management page that replaced an old ASP.NET/WebForms application. It was decided to make this application considerably more user friendly and more responsive - effectively we wanted to give it a good technology update.

The web designer did a great job putting together the new UI/UX - lots of modal pop-ups that allowed customers to change their settings and partial page refreshes to display those changes as they took place.

One key aspect that needed to be retained was the validation rules that prevented duplicate user names as well as duplicate email addresses from being saved in the database. These validations were to be handled consistently across the application so they were added as remote validations within the model class as illustrated below.
[Required(ErrorMessage = "Email Address is required.")]
[StringLength(100, ErrorMessage = "Email Address must be between 5 and 100 characters in length.",MinimumLength = 5)]
[RegularExpression( @"<trimmed for brevity", ErrorMessage = "Please enter a valid Email Address.")]
[Display(Name = "Email Address")]
[Remote("CheckEmail", "Home", AdditionalFields = "UserName", ErrorMessage = "This email has already been registered.  Please enter a different Email Address")]
[DataType(DataType.EmailAddress)]
public string EmailAddress { get; set; }

When using the [Remote] attribute JQuery Validation will, during the validation process, fire off an $.ajax() call to the controller/method you indicate. The method should be defined similar to that below:
public JsonResult CheckEmail(string emailAddress, string userName)
{
   try
      {
         if (this.repository.GetAccountEmail(userName).ToLower().Equals(emailAddress.ToLower()))
         {
            // this is fine, use it
            return this.Json(true, JsonRequestBehavior.AllowGet);
         }

         if (this.repository.IsEmailRegistered(emailAddress))
         {
            return this.Json(false, JsonRequestBehavior.AllowGet);
         }
      }
      catch (Exception exception)
      {
         this.logging.Error("Home/IsEmailAddressRegistered", exception);
         // don't throw...otherwise we'll send over default error page back to the .ajax call.
      }
   return this.Json(true, JsonRequestBehavior.AllowGet);
}

Basically a question is being asked - and a TRUE/FALSE response is required for the validation to fire correctly.  In this instance you'll notice that I attached the UserName in the call - this was because the email belonging to the user being edited was of course valid and exempt from the duplicate email rule.

In most cases the validation from this controller method will return an answer well before the user attempts to submit any changes back the server (email is second field on the add form, while first on the edit form).  However, there remains a slight problem with the JQuery $.validator object. When it invokes this remote validation on the server it won't wait for the return before returning a result when $(form).valid() is called.  It will provide a dirty answer - in other words, it will lie to you.  And end users will find these holes by simply entering an email and then clicking the button that fires off the save.

According to the project members of $.validate project on GitHub this isn't a bug, but a feature. While I agree with the logic presented in the ticket, I couldn't seem to find a solution within the validation documentation that would submit the data on the form to the server after all the validations have returned. In fact the JQuery validation documentation references that the submitHandler event handler should be the place where you do an $.ajax() form post after its been validated. Problem here is that submitHandler is invoked even when there are still pending requests in $.validator object. It seems no matter you do .valid() will return invalid responses until all the pending requests have been completed.

To combat this I found some code that will continue to process until two conditions are satisfied.

  • $.validator.pendingRequest counter must be zero 
  • .valid() method returns true.

The method below will get a handle to the form's $.validator() object. It then examines the pendingRequest value, if it is not zero, the function will exit. If it is zero, all validation $.ajax() requests have returned and its now safe to check the valid() method.  If it returns true we can safely post the data to the server.
function waitForAddFormValidation() {
   var validator = $('#addAccountForm').validate();

   if (validator.pendingRequest === 0) {
      clearInterval(interval);
      if ($('#addAccountForm').valid()) {
         // push your data or form submit
      }
   }
 }
You'll notice above that I tell the browser to stop calling the "waitForAddFormValidation" once the pendingRequest is set to zero by calling the "clearInterval" method.

Then during the button click event setup the method above to be fired at precise intervals.  This is illustrated below.
// Fired from the "save" button on the add account modal
 $("#addAcctAddBtn").click(function(event) {
    if (!$('#addAccountForm').data('changed')) {
        $('#addAccountModal').modal('hide');
            return;
        };

        $('#addAccountForm').valid();

        interval = setInterval(waitForAddFormValidation, 30);
});

In this instance I am first telling the form to validate - which will fire all the validations, including our troublesome $.ajax() call to Home/CheckEmail. Then I instruct the browser to invoke "waitForAddFormValidation" every 30 milliseconds until I tell it to stop.

In going this route I felt like I was abusing the $.validator object a bit - but frankly this worked well and I couldn't find any better idea to get around this problem.

No comments:

Post a Comment