Skip to content

Instantly share code, notes, and snippets.

@james-jlo-long
Last active August 29, 2015 13:57
Show Gist options
  • Save james-jlo-long/9365492 to your computer and use it in GitHub Desktop.
Save james-jlo-long/9365492 to your computer and use it in GitHub Desktop.
A brief list of JavaScript resources

JLo's JavaScript Resource List

Or "Everything you know about JavaScript is wrong."

Managing jQuery Validate

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.

Never bind functionality to a styled class

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment