The Epic, Awesome & Supremely Useful Data Attribute

Ok, perhaps, just perhaps, the title of this article is a bit overblown. What I’m talking about today isnt as sexy as Unreal in the browser.

You won’t believe how many clients ask for this…

You won't believe how many clients ask for this...

Nor is it as cool as a browser-based drum machine.

I bet I can validate a lot of forms with this…

I bet I can validate a lot of forms with this...

However, I’ve always been more of a fan of the practical side of web development. Don’t get me wrong – I love to see the browser pushed farther and farther along, but at the end of the day, I tend to get a lot more excited about things that I think I may actually use one day.

I’d like to spend a few minutes today talking about one of the more simple features available to web developers – data attributes.

Data Attributes

Data attributes refer to attributes added to an HTML tag that begin with data-. So, for example, in the snippet below, there are two data attributes:

<img src="kitten.jpg" data-cuteness="10" data-furry="true">

When the browser encounters this tag, it recognizes the two attributes that begin with data- as data attributes and then promptly ignores them. This is still valid HTML but the browser simply considers the data attributes as additional attributes that have no meaning by themselves.

While the browser ignores these values, they exist in the DOM and are visible to both JavaScript and CSS. Before discussing how you can access these values, let’s consider a simple example of where using data attributes can improve an application.

Our Demo

For our first demo, we’re going to build a simple product viewer. We’ll show a few product images and when the user puts their mouse over the image, information about the product should be loaded and displayed to the user. Let’s start with an initial version. It will include the images, an empty div, and some basic JavaScript to recognize mouse movement over an image.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
    </head>
    <body>

    <h2>Products</h2>
    <p>Hover over an image for more information!</p>

    <img src="imgs/product1.png" class="productImage" />
    <img src="imgs/product2.png" class="productImage" />
    <img src="imgs/product3.png" class="productImage" />

    <div id="productArea"></div>

    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
    <script>
     $(document).ready(function() {
        $(".productImage").on("mouseover", function(e) {
            //figure out what image we're over and get data about it
        }); 
     }); 

    </script>
    </body>
</html>

In the example above, we’ve got three images. They are hard coded in the HTML but you can imagine them as having been generated by a server-side application running Node, ColdFusion, or PHP. We’ve also got some JavaScript that will listen for mouseover events on any of the images.

So now ask yourself – considering that each image represents a product in your catalog (stored in a database, a NoSQL solution, etc, it doesn’t matter), how do we associate an image with the record from the back end? I know, for example, that product1.png represents a product in my database with the primary key of 1. I can build a simple back end service that lets me pass in a product primary key and it will return information about that product.

Possible Solutions

So far good. So I need to associate “1” with my image (and obviously, the other product IDs with the other images). How do we do that? I could use an alt tag:

<img src="imgs/product1.png" alt="1" class="productImage" />

But the alt tag has particular meaning for the browser. If we check MDN’s img tag documentation, we see that the alt attribute is used when the image URL is wrong or the image is in the wrong format.

Ok, so, how about using CSS? If you specify a CSS class that doesn’t exist, then it won’t have any impact on the display. So we could do this:

<img src="imgs/product1.png" class="productImage 1" />

Except of course that this isn’t valid CSS. If your curious, you can check the CSS grammar rules, but the basic issue is that you can’t start a class name with a number. Ok, fine then, lets do this:

<img src="imgs/product1.png" class="productImage productId_1" />

This works. Now when we mouse over an image we can use the classList API to fetch all the classes on the element, iterate, see if a class begins with productId, split on _, and we gt our ID finally. Messy, but I’ll be honest – I’ve done much worse.

Of course, while it works, it can lead to trouble later on. Imagine we’ve run into a display issue and our designer is carefully pouring over the source to figure out what’s wrong. The additional CSS, while certainly not the cause of the problem, is noise. It gets in the way and makes the designer check to see if is part of the problem. So why not consider yet another workaround?

<input type="hidden" name="product" value="1" />
<img src="imgs/product1.png" class="productImage" />
<input type="hidden" name="product" value="2" />
<img src="imgs/product2.png" class="productImage" />    
<input type="hidden" name="product" value="3" />
<img src="imgs/product3.png" class="productImage" />

In the snippet above, I’ve added hidden form fields before each image. The name isn’t important but the value is. Now I can get my product ID by using DOM traversal techniques. In this case, the hidden form field I want is the immediate item before the image in the DOM. Here is a full example:

Listing 1.2 (demo2/index.html)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
    </head>
    <body>

    <h2>Products</h2>
    <p>Hover over an image for more information!</p>

    <input type="hidden" name="product" value="1" />
    <img src="imgs/product1.png" class="productImage" />
    <input type="hidden" name="product" value="2" />
    <img src="imgs/product2.png" class="productImage" />    
    <input type="hidden" name="product" value="3" />
    <img src="imgs/product3.png" class="productImage" />

    <div id="productArea"></div>

    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
    <script>
     $(document).ready(function() {
        $(".productImage").on("mouseover", function(e) {
            //get my productId
            var prodId = $(this).prev().val();
            console.log(prodId);
        }); 
     }); 

    </script>
    </body>
</html>

In this case, jQuery makes it a bit easier to get. This works too.

Using hidden form fields for metadata

But again – while this solution works – it adds mess to our DOM. You can certainly live with it, and this is exactly the type of thing I’ve done myself in the past, but certainly there’s a better way.

Solving with Data Attributes

This is where data attributes come into the picture. Consider this version:

<img src="imgs/product1.png" class="productImage" data-product-id="1" />
<img src="imgs/product2.png" class="productImage" data-product-id="2" />
<img src="imgs/product3.png" class="productImage" data-product-id="3" />

That’s much nicer, right? So how do we use it? Before showing how jQuery makes it simpler, I think it’s important to see the real API first. First, let’s add a bit more data to our products, just so we can see additional properties while testing:

<img src="imgs/product1.png" class="productImage" data-product-id="1" data-type="car" />
<img src="imgs/product2.png" class="productImage" data-product-id="2" data-type="weapon" /> 
<img src="imgs/product3.png" class="productImage" data-product-id="3" data-type="car" />

We’ve now got a product-id and type data attribute for our products. If you mouseover a product and get it’s DOM element, the data will be available in the dataset attribute.

DOMStringMap

The data type for dataset is a DOMStringMap. What you need to know, though, is that it is iterable and can be treated like an object. In the code below, I’ve modified my mouseover to simply dump the dataset as well as iterate over each item:

$(".productImage").on("mouseover", function(e) {
    console.dir(e.currentTarget.dataset);
    for(var x in e.currentTarget.dataset) {
        console.log("attribute is "+x+" and the value is "+e.currentTarget.dataset[x]);
    }
}); 

Dumping the dataset

The most important thing to note here is what happened to the product-id value. When accessed via the DOM, it is made available as productId. Basically the DOM camel cased the value. This becomes important both for reading and writing. If I wanted to modify the product-id value, I’d use the camelCase version instead: e.currentTarget.dataset["productId"]++;.

Be careful to note the compatability section in the docs for DOMStringMap. They list Firefox 6 as being compatible and everything else has a question mark.

Compatability

It isn’t the data attributes that aren’t supported (I’ll show a better chart for that later in the article), but rather the API described here is not compatible. So for example, hasDataAttr and removeDataAttr will not work. Since you can treat this object as a simple object, then just checking for the values will be enough. Check out the updated example below:

$(".productImage").on("mouseover", function(e) {
    var dataset = e.currentTarget.dataset;

    console.log(dataset);

    //update the product id
    dataset["productId"]++;

    //remove type
    if(dataset["type"]) {
        delete dataset["type"];
    }
});

I’ve copied the dataset out for easier access and then demonstrate modifying and removing the set.

As an aside, why did I switch from console.dir to console.log? With dir, Chrome shows the current state of the object. So in my testing, it always showed the value of the dataset after the rest of the code fired. Using log converted the object to a string and showed me the values immediately. Obviously in real life debugging I could have used a breakpoint.

Modifying the dataset

Complex Values

Speaking of modifying the dataset, what about complex values? If you try to store something complex like like below:

dataset["productId"] = {"complex":[1,2]};

…then the value is run through toString before it gets stored in the DOM. Essentially you end up with “[object Object]”. You could, of course, JSONify the data, such as:

dataset["productId"] = JSON.stringify({"complex":[1,2]});

You’d then need to parse the data before using it, but my gut tells me that if you’re storing that much data in data attributes that you’re doing something wrong. (And again, that’s just my gut. I’d be willing to bet I’m wrong and would love to hear arguments in the comments!)

Using jQuery

Ok, so finally, let’s look at how jQuery works with data attributes. First, let’s look at a code snippet:

$(".productImage").on("mouseover", function(e) {
    var data = $(this).data();

    console.log(data);

    //update the product id
    var productId = $(this).data("productId");
    $(this).data("productId", productId+1);

    //remove type
    if(data.hasOwnProperty("type")) {
        $(this).removeData("type");
    }

    //complex for the hell of it
    $(this).data("complex", {name:"Ray", stuff:[1,2,3]});
    console.log(e.currentTarget.dataset);

    });
});

We’ve got a few things going on here. First, we use jQuery’s data method to fetch the dataset. You can then write to it using data(key,value). Finally, we see if type exists as a property of the data object and, if so, use removeData to remove it from the dataset.

So far so good. Now let’s take the red pill and store complex data. When this is run, it works. But… something interesting happens. Note in the image below that the first log prints out what jQuery’s data method returned. The second log at the bottom there is just printing out the “pure” DOM value for the dataset.

Complex data via jQuery

So… yeah. That’s fascinating. Notice how both the modified product value and the complex value don’t exist. jQuery is actually storing values in it’s own cache, which allows for things like complex data. That’s both cool and…worrisome.

Again, my advice is to avoid doing this. If I needed to work with complex data, I’d probably just use a simple JavaScript variable in the page that represents this data and is retrievable by some core value – like the product id. So the DOM has the product id with the image, and to get the complex data I would look it up via some global object.

Using Data Attributes with CSS

Stand back – I’m a developer using CSS! So it’s safe to assume that any and all of the CSS below won’t be pretty. I’m just going to demonstrate how CSS can notice, and modify, items based on CSS values. Let’s look at the complete source and then we’ll view it in the browser.

Listing 1.3 (demo6/index.html)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
    </head>
    <body>

    <h2>Products</h2>
    <p>Hover over an image for more information!</p>

    <img src="https://placekitten.com/g/200/300" class="productImage" data-product-id="1" data-type="car" />
    <img src="https://placekitten.com/g/200/300" class="productImage" data-product-id="2" />    
    <img src="https://placekitten.com/g/200/300" class="productImage" data-product-id="3" data-type="car" />


    <style>
     img[data-type] {
        -webkit-filter: sepia(1);
         filter:sepia(1);
     }   

    img[data-product-id="2"] {
        width:90px;
    }
    </style>
    </body>
</html>

I’ve switched from three dummy images to kittens (as an aside, switching to kittens is always recommended.) I’ve then used a bit of CSS to update the page.

The first block picks up on items that include a data-type attribute. For those we’ll add a pretty sepia effect. This should impact the first and last image. The second blocks looks for a product id with value of 2.

Now to be clear, this is no different than regular attribute specification, so there isn’t anything special going on here, but it’s nice to know it’s possible. Here’s how it renders in the browser.

I'm quitting my job to be a designer.

Compatability

Ok, time to clench our teeth and take a look at how compatible data attributes are. I’ve already mentioned how the DOMStringMap API isn’t very useful, but what about data attributes themself? For that – we’ll consult the always excellent CanIUse.com.

CanIUse data for data attributes

That’s pretty darn impressive in my opinion – pretty much 100%.

Hopefully you found this look at data attributes interesting. As I said in the beginning, it isn’t exactly rocket science, but is one those simple, useful, and darn practical things that more developers should be making use of!

Header image background courtesy of Shane Glenn

Comments

  • Pingback: Tweet Parade (no.19-2015) - Best Articles of Last Week | gonzoblog()

  • Kseso Css

    Hi Raymond.
    The value of a class or id (in html) attribute can begin with a number.
    Css specification, in the name of the selector, not allow the use of some characters (like numbers) at the beginning.

    To resolve conflicts (when the attribute value begins with one of these disallowed characters) CSS has a tool: “Escaping rules” to use as class or id selector.

    It is the character
    http://www.w3.org/TR/CSS21/syndata.html#characters

    For < … class=’a 1′> we can build the CSS selector in 2 ways
    .1 { }
    [class~=’1′] { }

    A demo on codepen
    http://codepen.io/Kseso/pen/JkqAr

    Greetings

  • burkeholland

    Great article. I remember creating my own “nonsense” HTML attributes before I knew about data. I used to just add “productId” as an attribute to my HTML elements! It worked, but I never felt good about it.

  • Jul

    I’ve used the name attribute (yeah tricky) to store data back in times. But the data- pattern attribute really rocks. And i just love the fact that it’s subject to CSS.