title: Progressive enhancement with handlers and enhancers url: https://hiddedevries.nl/en/blog/2015-04-03-progressive-enhancement-with-handlers-and-enhancers hash_url: ba6ef65c13ffa3b92e95fb486cfc6fe0
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.
On many small to medium sized websites, the required JavaScript can be drilled down to two types of usage:
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.
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?
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>
.
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.
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)
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).
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);
}
}
});
});
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:
data-handler
attributedata-enhancer
attributedata-handler
and data-enhancer
attributes respectively, and execute all functions when they are requiredThis 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