Separate and connected

If you’re writing JavaScript to work over existing HTML, you can make effective use of the HTML5 data attributes to allow your code to be generalised; that is, not written for that one-and-only bit of HTML.

There are a number of common libraries that use this technique, and really demonstrate the value of having general code whose behaviour can be customised by the HTML. My favourite example is Parsley, a form validation library which is initialised over your HTML form, and takes all its config and behaviour from the data attributes.

First, a quick recap on what data attributes are. HTML5 allows you to add an attribute that starts with data- to any element, which essentially allows you to specify custom — but still valid — attributes. For example:

<div id="mydiv" data-info="some info about this div">...</div>

This is useful because any JavaScript you have on the page can extract information from or store information in those tags. This allows the HTML author to specify information that a generalised JavaScript library can use to determine how to behave on that page.

Let’s do a couple of worked examples to see this in action.

Control how text is highlighted

Suppose we want a very simple JavaScript library which can highlight blocks of text when you hover over them, and we want the block of text to tell us what colour to highlight.

Maybe this would be useful in a CMS where we want the content writer to have control over the highlight colour. Or maybe the colour of the text comes from a database, and we can’t know it in advance to include it in our CSS.

Our HTML could be like this:

<div class="highlightable"

data-highlight="#ff0000">

Highlight this text

</div>

Our first step is to come up with a function that can read the data-highlight attribute and set the background of the div appropriately. Something like this:

function(element) {

var jqel = $(element);

var colour = jqel.attr("data-highlight");

jqel.css("background-color", color);

}

This takes a DOM element, and uses jQuery to first extract the value of the data-attribute and then to set it as the background-color in CSS.

Let’s immediately wrap that in a jQuery event handler, so we can see it in action:

$(".highlightable").on("hover", function() {

var jqel = $(this);

var colour = jqel.attr("data-highlight");

jqel.css("background-color", color);

});

We’ve bound a hover event to the elements in the page with class highlightable and we’ve replaced the explicit pass of element with the use of this as provided by jQuery.

If you implement this you’ll see immediately that the element changes colour when you hover over it, but not when you move your mouse out again; it’ll just remain highlighted.

This gives us an excuse to store data in the element using an attribute.

We’ll do this by binding an event handler to the mouseenter and mouseleave events using jQuery’s shortcut:

$(".highlightable").on("hover",

function() {

// function for mouseenter

},

function() {

// function for mouseleave

}

});

The first function is run when the mouse enters the area of the element, and the second function is run when the mouse leaves it.

Now we augment our function for setting the highlight to also record the original colour of the element:

$(".highlightable").on("hover",

function() {

var jqel = $(this); var originalColour = jqel.css("background-color");

jqel.attr("data-original", originalColour); var highlightColour = jqel.attr("data-highlight");

jqel.css("background-color", highlightColour);

},

function() {

// function for mouseleave

}

});

When this first function runs, it will first extract the current colour from the CSS of the element, and store it in the element under data-original before then going on to set the highlight. In the DOM, our div now looks like this:

<div class="highlightable"

data-highlight="#ff0000"

data-original="#ffffff"

style="background-color: #ff0000">

Highlight this text

</div>

To handle the mouseleave event, all we need to do is reverse this process:

$(".highlightable").on("hover",

function() {

var jqel = $(this); var originalColour = jqel.css("background-color");

jqel.attr("data-original", originalColour); var highlightColour = jqel.attr("data-highlight");

jqel.css("background-color", highlightColour);

},

function() {

var jqel = $(this);

var originalColor = jqel.attr("data-original")

jqel.css("background-color", originalColour);

}

});

We’ve now extended our second function to read back the value of data-original and to revert the CSS to the original colour.

Show and Hide Page Sections

Let’s do a slightly more complex worked example from a real-world use case that you are bound to run into eventually: showing and hiding page sections based on button clicks.

We’ll start by imagining what the HTML would look like to allow us to do this generally. Something like:

<a href="#"

data-blink="controller"

data-blink-controls="my-content"

data-blink-init="hidden">

Show

</a> <div data-blink="container" data-blink-id="my-content">

This content can be shown and hidden.

</div>

How the above HTML looks in your browser

First thing to note is that we’ve started including the word blink into our data attributes. The reason for this is to provide a namespace for our general application, which we’re going to call “blink”. It means if there are any other data attributes on the page for other libraries, we can avoid our application accidentally tripping over on them. This is general good practice that’s worth following.

Here we’ve defined an anchor tag which has three properties, all of which are specific to our application (i.e. they are not special features of HTML):

data-blink=”controller” — this tells us the anchor tag is something our application will call a “controller”, that is, it’s going to control the show/hide

— this tells us the anchor tag is something our application will call a “controller”, that is, it’s going to control the show/hide data-blink-controls=”my-content” — this tells us the id of the content that the link will show/hide

— this tells us the id of the content that the link will show/hide data-blink-init=”hidden” — this tells us that the content should be hidden initially

We then have a div which also has some properties

data-blink=”container” — this tells us that this is the container for the content we want to show/hide

— this tells us that this is the container for the content we want to show/hide data-blink-id=”my-content” — this gives the container an id for us to find it by.

The behaviour we want is that when you click the “Show” link, then the related container will appear, and the link will be replaced by “Hide”. Then when you click “Hide” the related container will disappear and the link will once again say “Show”.

Lets build up this code step-by-step. At the end of this post is the full functioning code-snippet.

First, set up the framework for the code on the page:

<script type="application/javascript">

function init() {

// implementation will go here

}



jQuery(document).ready(function($) {

init();

});

</script>

Now we’ll implement the init function which will work in two parts:

Set the page up in its initial state Add the event handler to show/hide the containers

Here’s a block of code that implements our initial state set-up:

function init() {

var controllers = $("[data-blink=controller]");

for (var i = 0; i < controllers.length; i++) {

var controller = $(controllers[i]);

var id = controller.attr("data-blink-controls");

var initialState = controller.attr("data-blink-init");

var container = $("[data-blink-id=" + id + "]");

if (initialState === "hidden") {

container.hide();

} else if (initialState === "shown") {

container.show();

}

}

}

Going through that in a bit more detail:

var controllers = $("[data-blink=controller]");

for (var i = 0; i < controllers.length; i++) {

var controller = $(controllers[i]);

This finds all the page elements which declare themselves to be a “controller” for our application, by using an attribute-based selector. We then go through each controller, which I do with a for loop partly for explicitness in the example, and partly because I’m old fashioned.

var id = controller.attr("data-blink-controls");

var initialState = controller.attr("data-blink-init");

This reads the data attributes to get the id of the container that this element controls, and finds out what we want the initial state of that container to be.

var container = $("[data-blink-id=" + id + "]");

if (initialState === "hidden") {

container.hide();

} else if (initialState === "shown") {

container.show();

}

Finally we get a handle on the container and either show or hide it according to the desired initial state.

Next we need to bind an event handler to our controllers which will trigger the show or hide mechanism. Again, here it is in full, then we’ll step through it in more detail:

function init() {

var controllers = $("[data-blink=controller]");



// ... init code from above



controllers.on("click", function() {

var controller = $(this);



var id = controller.attr("data-blink-controls");

var state = controller.attr("data-blink-state");

var initialState = controller.attr("data-blink-init");



if (state === undefined) {

state = initialState;

}



var newState = state === "hidden" ? "shown" : "hidden";



var container = $("[data-blink-id=" + id + "]");

if (newState === "hidden") {

container.hide();

controller.html("Show");

} else if (newState === "shown") {

container.show();

controller.html("Hide");

}



controller.attr("data-blink-state", newState);

});

}

Focussing on the new part of this init function, then:

controllers.on("click", function() {

var controller = $(this);

Bind a function on the click event, and get a handle on the controller from jQuery’s this reference.

var id = controller.attr("data-blink-controls");

var state = controller.attr("data-blink-state");

var initialState = controller.attr("data-blink-init");

Extract the three attributes that we want from the controller:

The id of the container that the controller controls

The current state of the container (and note that this may not yet be set)

The initial state of the container (which we may use if the current state is not set)

if (state === undefined) {

state = initialState;

}

Default the state to the initial state if it is not set

var newState = state === "hidden" ? "shown" : "hidden";

Determine what the new state is; use this as an excuse to use the ternary operator rather than the longer form if/else, to flip the state from one to the other.

var container = $("[data-blink-id=" + id + "]");

if (newState === "hidden") {

container.hide();

controller.html("Show");

} else if (newState === "shown") {

container.show();

controller.html("Hide");

}

Get hold of a reference to the container and then do two things:

Set the visibility to shown or hidden

Update the text of the controller so that it shows the appropriate action word. That is, if the controller is hidden, have the link say “Show” and if it is shown, have the link say “Hide”.

controller.attr("data-blink-state", newState);

Finally, we set or update the data-blink-state attribute on the controller so that we know what state the container is in for future reference.