One of the 50 or so tasks I’m juggling at my day job deals with coming up with UI standards for one of our applications. I’m trying to think through every common scenario we have, design how it should work from a UI stand point (both the perceived UI as well as the HTML markup and CSS), and create examples to illustrate the standards. One of the scenarios I thought of was forms and validation. Ugh. I have spent the better part of a day now fighting with the jQuery Validation plug-in. It is the defacto standard for validation in jQuery. While it is powerful, and I really like being able to specify validation rules by simply applying CSS classes, it leaves a lot to be desired in the area of flexibility.
What I wanted
Here’s what I wanted for our forms and validation: fluid width, labels for each element, and (hidden) labels for element validation messages when needed. If I didn’t want to display a validation message, I just wanted to be able to omit the label. I didn’t want to have to do a bunch of JavaScript config. I also wanted exact control over how the error labels were rendered. Sometimes I wanted error labels to have a special class or other styles. Basically, I wanted to be able to write zero JavaScript code to get a nicely-formatted, validated form that I had complete control over.
Out of the box, while it does allow you to use your own error labels, the Validation plug-in is very dumb in how it uses them. First, it strips all CSS classes from the labels when it unhides them. Very, very annoying. There does not seem to be a good way to override this behavior without resorting to major hacking (like I did below). Second, if you do specify a custom label, it will only use it for the first validator applied to an element. If you have multiple validators applied to a single element, it will still add a new label element for the second validator. I ran into this as soon as I tried to have a required E-mail field. Ugh.
The good news is that it is possible to get the simple layout I wanted with minimal JavaScript config. The bad news is that it was an incredible pain in the rear to get there and involved a lot of browsing through the Validation plug-in’s source code.
How I did it
One of the nice things about jQuery Validation is that you can specify defaults that will apply to all instances. So once I figured out how to get the behavior I wanted, it was easy to encapsulate it where the entire application would be able to leverage it easily. So what’s the solution? Fugliness.
First, you need to specify a showErrors callback. When specified, this is invoked and allows you to fully control the display of error messages. It receives two parameters, an errorMap which is useless, and an errorList that contains validation errors. My approach was to loop through the errorList, get the name of each element, look for a corresponding label element with class error, and unhide it. This allowed me to fully control validation messages via markup, and it made it easy to choose not to display a validation message at all. Yes, this does mean that I lost the ability to display a different error message for different validators (such as one when an E-mail field is blank versus when it has an invalid address), but I prefer to have a single validation message for each element anyway.
Unhiding the error label is only part of the solution. You also need to highlight the element that the error corresponds to. This can be achieved by using the optional highlight callback. In this callback, you can add the error class. Be warned, you must manually call your own callback from your showErrors callback, otherwise highlight and the corresponding unhighlight callbacks will never be invoked. This is the part that is fugly.
Here’s the full source code, which I wrapped in a .js file and included alongside the main jQuery Validation .js file. This insures consistent behavior everywhere we use the jQuery Validation plug-in in our project.
$.validator.setDefaults({ showErrors: function(errorMap, errorList) { var validation = this; $(errorList).each(function() { var error = this; var errorLabel = $("label[for='" + error.element.name + "'].error"); errorLabel.css("display", "inline-block"); validation.settings.highlight(error.element); }) for (var i = 0, elements = this.validElements(); elements[i]; i++) { validation.settings.unhighlight(elements[i], validation.settings.errorClass, validation.settings.validClass); } }, highlight: function(element) { $(element).addClass("error"); }, unhighlight: function(element) { $(element).removeClass("error"); $("label[for='" + element.name + "'].error").hide(); } });
This is the final product:
And the corresponding markup:
<fieldset> <legend>A simple comment form with submit validation and default messages</legend> <div class="wrapper"> <p> <label for="name"> Name:</label> <input type="text" id="name" name="name" size="25" class="required" minlength="2" /> <label for="name" class="error"> Please enter a valid name.</label> </p> <p> <label for="email"> E-Mail:</label> <input type="text" id="email" name="email" size="25" class="required email" /> <label for="email" class="error"> Please enter a valid E-mail address.</label> </p> <p> <label for="url"> URL:</label> <input type="text" id="url" name="url" size="25" class="url" value="" /> <label for="url" class="error"> Please enter a valid URL.</label> </p> <p> <label for="state"> State:</label> <select name="state" id="state" class="required"> <option></option> <option>Tennessee</option> <option>Georgia</option> <option>South Carolina</option> </select> <label for="state" class="error"> Please select a state.</label> </p> <p> <label for="terms"> </label> <input type="checkbox" name="terms" value="terms" class="required" /> <label for="terms" class="checkbox">I have read and accept the terms.</label> <label for="terms" class="error">You must accept the terms.</label> </p> <p> <label for="comment" class="textarea"> Your comment:</label><br /> <textarea id="comment" name="comment" cols="22" class="required"></textarea> </p> <p> <input class="submit" type="submit" value="Submit" /> </p> </div> </fieldset>
It’s not perfect, but I’m happy with the markup, and I’m happy with the UI. Hopefully I didn’t miss something obvious with the Validation plug-in that would have saved me half a days work. If I did, feel free to tell me how stupid I am. 🙂