? Progressive enhancement with handlers and enhancers (archive)

Source originale du contenu

Recently I adopted a different way to manage bits of JavaScript in websites I was building. It exercises progressive enhancement (PE) by declaring handler and enhancer functions on HTML elements.

TL;DR: When JavaScript is used to handle user interactions like clicks, or enhance the page by manipulating the DOM, traditionally you’d have JavaScript find that HTML element in your page, and hook some code into it. But what if you’d switch that around, and have the HTML element “tell” JavaScript what function to execute?

How? I declare JavaScript functions on HTML elements. Separating them to functions that happen on click and those that happen on page load, I use two attributes (data-handler and data-enhancer) that both get space-separated function names as their values. Then, with a little bit of JavaScript, I make sure the functions execute when they need to.

Note that this works best for websites for which the mark-up is rendered on the server, to which JavaScript is added as an extra layer. Websites that rely on JavaScript for rendering content will probably have options to do the same within the framework they are built with.

Separating to handlers and enhancers

On many small to medium sized websites, the required JavaScript can be drilled down to two types of usage:

  1. things that need to happen on a click
  2. enhancements after the inital page load

Surely, there are often other events we want to use for triggering JavaScript (scroll, anyone?), but let’s focus on these two types first.

Many simple websites will have both types of script in a single scripts.js file, which is also used to query the nodes interactions need to happen on, and to add click handlers to those nodes.

This year, Krijn and Matijs introduced me to a new way to go about this. It has proven to be a very powerful pattern in some of my recent projects, which is why I’d like to share it here. Credits for the pattern, and for the initialisation functions I will discuss later, go to them.

Including JavaScript functions to the declarative model

The pattern starts from the idea that web pages have three layers to them: structure (HTML), lay-out (CSS) and enhancement (JavaScript). HTML is a declarative language: as an author, you declare what you’d like something to be, and that is what it will be.

Let this be a header, let this be a list item, let this be a block quote.

This is great, browsers can now know that ‘this’ is a list item, and treat it as such. Screenreaders may announce it as a list, your RSS reader or mobile Safari “reader view” can view it as a list, et cetera.

With CSS we declare what things look like:

Let headers be dark blue, let list items have blue bullets and let block quotes render in slightly smaller type.

This works rather well for us, because now we don’t need to think about what will happen if we add another list item. It being a list item, it will have the CSS applied to it, so it will have blue bullets.

The idea I’d like to share here, is that of making JavaScript functions part of this declarative model. If we can declare what something looks like in HTML, why not declare what its behaviour is, too?

Handlers: things that happen on a click

The idea is simple: we introduce a data-handler attribute to all elements that need to trigger function execution. As their value, we add one or more functions name(s) that need(s) to execute on click.

For example, here’s a link:

<a href="#punk-ipa">More about Punk IPA</a>

This is an in-page link to a section that explains more about Punk IPA. It will work regardless of JavaScript, but can be improved with it.

Cooler than linking to a section about Punk IPA, is to have some sort of overlay open, with or without a fancy animation. We add a data-handler attribute:

<a href="#punk-ipa" data-handler="overlay">More about Punk IPA</a> 

In the data-handler, the value holds a function name, in this case ‘overlay’. The idea is that the overlay function executes when the link is clicked. Naturally, you would be able to add more than one function name, and separate function names by spaces. This works just like class="foo bar".

Within the function declaration, we will know which element was clicked, so we can access attributes. We can access the href or any data attribute. With that, it can grab the content that’s being linked to, append it into some sort of overlay, and smoothly transition the overlay into the page.

Note that this is similar to doing <a onclick="overlayfunction(); anotherfunction();">, but with the added benefit that a data-handler only gets meaning once JavaScript is active and running, and that it contains strings like CSS classes, instead of actual JavaScript code. This way, the scripting is separated in the same way as the style rules are.

Also note that is best practice to only add handlers to HTML elements that are made for click behaviour, like <button>s and <a>.

Adding function definitions

In our JavaScript (e.g. an included scripts.js file), we add all functions for click behaviour to one object:

var handlers = {
    'function-name' : function(e) {
         // This function is executed on click of any element with 
         // 'function-name' in its data-handler attribute.
         // The click event is in 'e', $(this).attr('data-foo') holds the
         // value of data-foo of the element the user clicked on 
    },
    'another-function-name' : function(e) {}
};

Those working in teams could consider making the handler object global. That way functions can have their own files, making collaboration through version control easier.

Adding click handling: one handler to rule them all

If we set all our click-requiring functionality up within data-handler attributes, we only need to attach one click handler to our document. Event delegation can then be used to do stuff to the actual element that is clicked on, even when that actual element did not exist on page load (i.e. was loaded in with AJAX).

This function (jQuery) can be used to handle clicks, then search through the handler functions and apply the ones specified in the data-handler function:

$(function() {
    'use strict';
    // generic click handler
    $(document).on('click', '[data-handler]', function(event) {
        var handler = this.getAttribute('data-handler');
        // honour default behaviour when using modifier keys when clicking
        // for example:
        // cmd + click / ctrl + click opens a link in a new tab
        // shift + click opens a link in a new window
        if (this.tagName === 'A' && (event.metaKey || event.ctrlKey || event.shiftKey)) {
            return;
        }
        if (handlers && typeof handlers[handler] === 'function') {
            handlers[handler].call(this, event);
        }
        else {
            if (window.console && typeof console.log === 'function') {
                console.log('Non-existing handler: "%s" on %o', handler, this);
            }
        }
    });
});

(Source; see also its vanilla JavaScript version)

Enhancers: things that happen after page load

We can run functions to enhance elements in a similar way, by adding their function names to a data-enhancer attribute. The corresponding functions go into a enhancers object, just like the handlers object above.

For example, a page element that needs to display tweets. Again, here’s a link:

<a href="https://twitter.com/bbc">Tweets of the BBC</a>

To enhance this link to the BBC’s tweets, we may want to load a widget that displays actual tweets. The function to do that may add some container <div>s, and run some calls to the Twitter API to grab tweets. To trigger this function:

<a href="https://twitter.com/bbc" data-enhancer="twitter-widget">
Tweets of the BBC</a>

To find out whose Twitter widget to display, our function could analyse the URL in the href attribute, or we can add an extra attribute:

<a href="https://twitter.com/bbc" data-enhancer="twitter-widget" 
data-twitter-user="bbc">Tweets of the BBC</a>

Another example: of a large amount of text, we want to hide all but the first paragraph, then add a “Show all” button to show the remainder. The HTML will contain all of the content, and we will hide the remainder with JavaScript.

<section>
    <p>Some text</p>
    <p>Some more text</p>
    <p>Some more text</p>
</section>

To the block of text we add a data-enhancer function that makes sure everything but the first paragraph is hidden, and a “Show all” button is added.

<section data-enhancer="only-show-first-paragraph">
    <p>Some text</p>
    <p>Some more text</p>
    <p>Some more text</p>
</section>

A function named ‘only-show-first-paragraph’ could then take care of removing the content, and adding a button that reveals it (this button could have a data-handler for that behaviour).

Running all enhancements

Assuming all our enhancer functions are in one enhancer object, we can run all enhancers on a page with one function. The function looks for all elements with a data-enhancer attribute, and calls the appropriate functions.

$(function() {
    'use strict';
    // kick off js enhancements
    $('[data-enhancer]').each(function() {
        var enhancer = this.getAttribute('data-enhancer');
        if (enhancers && typeof enhancers[enhancer] === 'function') {
            enhancers[enhancer].call(this);
        }
        else {
            if (window.console && typeof console.log === 'function') {
                console.log('Non-existing enhancer: "%s" on %o', enhancer, this);
            }
        }
    });
});

Wrap-up

So the basic idea is: functions for click behaviour are handlers, and those that happen on page load are enhancers. They are stored in a handlers and enhancers object respectively, and triggered from HTML elements that have data-handler and data-enhancer attributes on them, to declare which functions they need.

In summary:

  1. All functions that need to execute on click (or touch or pointer events), are declared on the HTML element that should trigger the click, in a data-handler attribute
  2. All functions that need to change/enhance stuff on the DOM, are declared on the HTML element they need to change, in a data-enhancer attribute
  3. Two JavaScript functions run through data-handler and data-enhancer attributes respectively, and execute all functions when they are required

Thoughts?

This pattern is not new, similar things have been done by others. Rik Schennink wrote about controlling behaviour before, and his conditioner.js deserves special mention. It is a library that not only manages modules of JavaScript, it also activates them based on responsive breakpoints, something the pattern described here does not do out of the box (it could).

For me and teams I worked in, the above has proven to be a useful way to declare actions and enhancements within pages. It adds maintainability, because it helps keeping functions organised. It also promotes reusability: although new functions can be added for each use, multiple elements can make use of the same function.

The method is not perfect for every project, and it can definitely be improved upon. For example, we could add the function that triggers all data-handler functions to an enhancer (<html data-enhancer="add-handlers">), as it is an enhancement (of the page) by itself. In big projects with many people working on the same codebase, it may be useful to have all functions in separate files and have a globally available handlers object.

Update 07/04/2015: I have opened comments, would love to hear any feedback