Code Time with Cody

$ dd if=/usr/cody of=www

Custom Remote Validation With jQuery

If you are using home-grown client-side validation, stop immediately. Take 15 minutes to grab a validation plugin such as the jQuery Validation Plugin. It already takes care of a lot of cases you haven’t yet realized you need. The purpose of this post is not to introduce the plugin, so I will now dive in and assume the reader is already using the jQuery validation plugin.

Remote Methods

The plugin has remote validation built right in. For example, say you have a user-registration page, and you want to make sure that the email address the user supplies is not already taken. The documentation gives the following example code:

1
2
3
4
5
6
7
8
9
$("#myform").validate({
  rules: {
    email: {
      required: true,
      email: true,
      remote: "check-email.php"
    }
  }
});

Now when validation is triggered, it will make sure that the email field is there (required), that is of valid email format (email), and then once those are done, it will make a remote call to check-email.php. That php file will get a request parameter called “email” with a value such as “someguy@aol.com”. It should return a JSON true if the provided email is valid. If the email is invalid, it can either return false or an error message to display to the user.

Advanced Remote Methods

Often the previous example is exactly what you need, and it is great that the plugin makes the usual case so easy. But what if remote validation of one field is dependent on another? Let’s say we have a page on which the user can order products. It has two <select> elements. One lets you pick a product, and the other lets you pick the timeframe in which you will need the product. So, a user might say

I need an Easy Bake Oven in 4-6 weeks.

We want to setup remote validation that checks that a product will be available in that timeframe. (This is a contrived example. Let’s ignore the user experience issues present n it.) Since the remote validation method accepts any arguments the jQuery ajax method accepts, a first pass at this is fairly straightforward.

(myapp.products.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$("#myform").validate({
  rules: {
      product: {
          required: true
      },
      timeframe: {
          required: true,
          remote: {
              url: "check-timeframe.php",
              data: {
                  product: function() {
                      return $("#product").val();
                  }
              }
          }
      }
  }
});

Now check-timeframe.php will get two request parameters: product and timeframe. We are explicitly adding product as a request parameter to the AJAX call. On the other hand, timeframe is added by the remote method of the validation plugin, because it is the field being validated.

This does almost exactly what we want it to do. But what happens when the user picks a timeframe but doesn’t pick a product? The product field is marked as required, so the user will get an error message for it. Your remote validation for timeframe will still fire (because the user did pick a value for timeframe), but check-timeframe.php won’t get any value for product. While it depends on your implementation, this will most likely result in an error message something like ”product is required”. So now the user is getting 2 error messages for forgetting to pick a product. What we really want is a way of only making the remote call for timeframe if a product is chosen.

Custom Remote Methods

We can add a method to the validator that delegates to the remote method, but only if certain conditions are met.

(myapp.productInTimeframe.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// if argument is a value, it will be returned
// if argument is a function, it will be evaluated, and the result will be returned
var asValue = function(valOrFunc) {
  return $.isFunction(valOrFunc) ? valOrFunc() : valOrFunc;
};

$.validator.addMethod("productInTimeframe", function(value, element, params) {
  // call asValue so 'product' can be either the value itself or
  // a function that returns the desired value
  var product = asValue(params.product);
  // product is a dependency for this validation
  // if it isn't populated, we can't validate
  if (!product) { // should probably be a little more careful than this
      return "dependency-mismatch"; // this is a special value for the plugin
  }

  return $.validator.methods.remote.call(this, value, element, {
      url: "check-timeframe.php",
      data: {
          product: product
      }
  });
});

Now the validator has a method called productInTimeframe. The argument to this method is an object with a product attribute that can be either a value or a function that will return the relevant value. If that product isn’t there, it says

My dependencies aren’t satisfied, so I can’t do my validation. You better check out those dependencies first and then we can talk.

It is easy to update our form validation to use this new method:

(myapp.products-custom-remote.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$("#myform").validate({
  rules: {
      product: {
          required: true
      },
      timeframe: {
          required: true,
          productInTimeframe: {
              product: function() {
                  return $("#product").val();
              }
          }
      }
  }
});

Now the user won’t get two error messages for forgetting to select a product. If he does select a product, the usual remote validation will occur, and if the product isn’t available in the selected timeframe, the timeframe select will show an error.

Hide the Details

This approach is a bit more verbose than simply using the built-in remote method, but it is the best way I have discovered to deal with this sort of scenario. Also, it is notable that the verbosity is contained within the productInTimeframe method itself. The arguments we are providing to the validate method for our form have actually been simplified. I think it is nice to have a reusable method that hides the details of the remote call.

Comments