Or "Everything you know about JavaScript is wrong."
jQuery Validate massively simplifies form validation. It's very extensible and will handle almost every situtation in which you can find yourself. But, if you're not careful, it can become an horrific plugin on a large site.
Without thinking, the errorPlacement
function can end up looking like this:
function errorPlacement(error, element) {
// If the element is inside the stock-checker table, the error need to go in the
// status column.
if (element.closest('td.mpn').length || element.closest('td.qty').length) {
error.appendTo(element.closest('tr').find('td').filter(':last'));
// The #compare-table is inside a .scrollable box so we need to move put in a
// special check to ensure the error appears after the input correctly.
} else if (element.closest('.compare-table').length) {
error.insertAfter(element);
// If the element is in a scrollable box, put the error below the box.
} else if (element.parents('.scrollable,.not-scrollable').size()) {
error.insertAfter(element.parents('.scrollable,.not-scrollable').filter(':first'));
// If the element is the enews t-and-c checkbox, put the error after the label.
} else if (element.parents('.enews-checkbox').size()) {
error.insertAfter(element.parents('form').find('.enews-tandc label'));
// If the element is a custom element, put the error after the drawn box. This
// should be quite low down in the tests so that we know the element shouldn't
// be affected by any of the other tests instead.
} else if (element.hasClass('is-invisible')
&& element.siblings('div[class^="style"]').size()) {
error.insertAfter(element.siblings('div[class^="style"]').filter(':first'));
// If the element is a datepicker, put the error after the button.
} else if (element.hasClass('hasDatepicker')) {
error.insertAfter(element.siblings('.ui-datepicker-trigger'));
// If the element is our custom file uploader, make the error appear under it.
} else if (element.hasClass('customfile-input')) {
error.insertAfter(element.parents('div').filter(':first'));
// Otherwise, business as normal.
} else {
error.insertAfter(element);
}
}
This means that in order to correctly place an error after an element, if the element is not a "special case", jQuery will have to traverse the DOM from the input
to the html
6 times. Not only have I seen this happen, I've done it. The theory was sound, there are many "special cases" to handle ...
A better approach is to have the element tell jQuery Validate what to do with the error:
function errorPlacement(error, element) {
var parent = element.parent(),
after = element.attr('data-errorafterparent'),
sibling = element.attr('data-erroraftersibling');
// Allow elements to suggest a parent element after which the error should
// be added.
if (after) {
parent = element.closest(after);
error.insertAfter(parent);
// Siblings work very similarly to parents, the difference being where the
// error is positioned.
} else if (sibling) {
parent = element.nextAll(sibling).first();
error.insertAfter(parent);
// If the element doesn't suggest anywhere for the error to go, insert it
// after the element (default placement).
} else {
error.insertAfter(element);
}
}
Now jQuery will only have to do any DOM traversal if the element has either a data-errorafterparent
or a data-erroraftersibling
attribute; if the element has neither, no traversing occurs. The code is smaller and more efficient. A similar trick can be done with the highlight
and unhighlight
methods:
function highlight(element, errorClass, validClass) {
var jQelem = element.type === 'radio' ?
this.findByName(element.name) :
$(element),
errorAttr = jQelem.attr('data-highlightelement');
// If the element has an error, trigger an event. This allows custom
// elements (such as dropdowns or checkboxes) show there is an error while
// keeping the coupling loose.
jQelem.
addClass(errorClass).
removeClass(validClass).
trigger('gainerror');
// If the element has described a custom element to highlight, highlight
// that element as well.
if (errorAttr) {
jQelem.
nextAll(errorAttr).
first().
addClass(errorClass).
removeClass(validClass);
}
}
function unhighlight(element, errorClass, validClass) {
var jQelem = element.type === 'radio' ?
this.findByName(element.name) :
$(element),
errorAttr = jQelem.attr('data-highlightelement');
// For the same reasons in the highlight method, trigger an event to allow a
// customised element to show there is no more error.
jQelem.
removeClass(errorClass).
addClass(validClass).
trigger('loseerror');
// Unhighlight any custom element.
if (errorAttr) {
jQelem.
nextAll(errorAttr).
first().
removeClass(errorClass).
addClass(validClass);
}
}
jQuery Validate now simply checks the element itself to see if any special cases are in play, using default handling if not.
Imagine a "buy now" button where clicking it will make the product image fly across the screen and POST product information to the server using AJAX.
<button type="button" class="btn btn-buy">Buy now</button>
Now imagine you want another "buy now" button that looks identical but doesn't do the JavaScript stuff. If you bound you JavaScript to .btn
or .btn-buy
, you will have to re-create the styles in order to make another button. A better approach is to use a JavaScript hook class, prefixed with js-
. These hooks have no styling and do not appear in the style sheets. Our button now looks like this (white space added for clarity):
<button
type="button"
class="btn btn-buy js-fly js-add-to-basket"
data-fly-from="#productImage"
data-fly-to="#basket"
data-add-data="#productInfo"
data-add-to="/basket">Buy now</button>
The JavaScript functionality is now looking for .js-fly
and .js-add-to-basket
. To re-create the button with neither of those piece of functionality, simply use the example higher up. Additionally, to create a button that adds the product to the basket via AJAX but does not fly across the screen, simply remove the js-fly
class and data-fly-
attributes:
<button
type="button"
class="btn btn-buy js-add-to-basket"
data-add-data="#productInfo"
data-add-to="/basket">Buy now</button>