Strategy

Instead of adjusting an HTML element’s visual style with jQuery, make the same changes by strategically adding and removing classes. The classes can be applied on the component, region, and page level to control any static state of the host element or its decedents.

Symptoms

You can easily detect when to use this strategy when you discover yourself performing any of the following actions:

Problem 1

The following code illustrates the first two of these symptoms:

$( document ).ready( function () {
    // Three separate elements are selected
    var loadingMessage = $( "#loadingMessage" ),
        header = $( "#header" ),
        content = $( "#content" );

    // Three CSS changes are made:
    loadingMessage.show();
    header.hide();
    content.hide();

    // Lots of setup and async code here

    function someReadyCallback () {
        // Three additional CSS changes are made
        loadingMessage.hide();
        header.show();
        content.show();
    }
});

Problem 2

This short example illustrates the third symptom:

var $notification = $( ".notification" );
if ( error ) {
    $notification.css( "color", "red" );
} else {
    $notification.css( "color", "green" );
}

Solutions

Solution 1

To visually adjust multiple items at the same time, follow these steps:

  1. Find a parent element that is shared by the elements you wish to adjust. Often you can use the html or body element, but it can be any element that contains all the target elements.
  2. Determine a good CSS class name to describe the combined visual state of the elements, optionally add that class to the HTML and then manipulate it by using jQuery’s addClass, removeClass and toggleClass methods. Prefixing these classes with state- is one way to signify their purpose.
  3. Write CSS that scopes selections using a combination of the chosen parent element and the new class that is being applied to it.

The CSS to fix our loadingMessage code would look like this:

/* Loading message is hidden by default, only shown during loading */
#loadingMessage { display: none; }
.state-loading #loadingMessage { display: block; }

/* Content and Header are hidden during loading */
.state-loading #content,
.state-loading #header { display: none; }

Since the page will always start in a loading state, we add the class of loading directly to the <body> element:

<body class="state-loading">

Finally, we delete almost all the code we had before, and just remove the loading class when loading is complete:

// Lots of setup and async code here

function someReadyCallback () {
    // The app is loaded!
    $( document.body ).removeClass( "state-loading" );
} 

Solution 2

To adjust the visual display of a single item, follow these steps:

  1. Any aspect of the visual display that will not change should be defined as the element’s default display.
  2. Determine a good class name to describe the new visual state of the element, optionally add that class to the HTML for the element and then manipulate it by using jQuery’s class manipulation methods.
  3. Write CSS that scopes the new styles to the chosen element with the new class applied. You can also use multiple class syntax if you the element you are styling is already defined by a class: .notification.ready (Multiple class syntax is not supported by IE6 and lower).

If we assume the default state of our notification display is green, we can use the following CSS:

.notification { color: green; }
.notification.state-error { color: red; }

The following code will add the class of state-error when our variable error is true:

var $notification = $( ".notification" );
$notification.toggleClass( "state-error", error );

Strengths

$( document )

    // perform the save, but not if we are still
    // loading or in the middle of a save
    .on( "click", "body:not(.loading, .saving) button.save", function () {      
        ...
    })

    // prevent forms from submitting
    // if a save is in progress
    .on( "submit", "body.saving", function ( e ) {
        e.preventDefault();
    })

    // only run the setup one time per element
    .on( "click", ".widget:not(.widget-setup)", function () {
        ...
        $( this ).addClass( "widget-setup" );
    });

Notes

Transitions

Often the move from one state to another is accompanied by some sort of animation. Since much of the time these transitions are visual finesse, I suggest using CSS3 transitions and animations to implement the transitions. The fallback in unsupported browsers will simply be an instant change from one state to the next. In situations where the animation is crucial to the interaction, you may not be able to leverage everything covered in this article.

Using Attributes instead of Classes

You can also use attribute changes to control flow, though I haven’t done any speed comparisons between the two methods. Here is an example using a custom HTML5 data attribute named data-state:

<body data-state="loading">
body[data-state="loading"] #loadingMessage,
body[data-state="saving"] #savingMessage { display: block; }
$body.attr( "state", "ready" ); // From loading to ready
// ... other code ...
$body.attr( "state", "saving" ); // Switch to saving

Education

If you work in a team environment, it would be wise to discuss this strategy with your team before implementing. JavaScript developers who do not work with CSS frequently may not understand the ramifications of the addClass and removeClass methods throughout your code.