jQuery: Storing and retrieving data related to elements

Posted

It’s very common to need to get information about a DOM element when a user interacts with it — for example, perhaps you have an unordered list of names, and when a user clicks on a name, you want to show a picture of the person above the list. In order to do this, you need to figure out which person the clicked list item represents. Many beginning jQuery users will attempt to achieve this by putting ID attributes on each list item, such as id="rebecca". Then, they’ll read the ID attribute off the clicked element and use it to build a URL for the related image.

<ul class="people">
<li id="paul">Paul</li>
<li id="rebecca">Rebecca</li>
<li id="alex">Alex</li>
<li id="adam">Adam</li>
</ul>


var portrait = $('#portrait');

$('ul.people li').click(function() { 
    var name = $(this).attr('id');
    portrait.html('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');
});

Strictly speaking, this will work. But is the ID really the right place to store this information? What if you need this behavior on other elements on the page too? You can’t have more than one element with the same ID on the page, so you might find yourself using funny prefixes in your IDs, like person_rebecca, and then stripping out the prefix. You could do it with classes, but then you’d have the opposite problem: you’re using (generally) unique classes like rebecca, but really classes are meant to indicate similarities among a set of elements. And what if you need to store more than one piece of information about an element on the element? Next thing you know you’ve got id="person_alex_red" and you’re jumping through all sorts of hoops to parse out the data you need.

Custom Data Attributes

HTML5 makes available [custom data attributes](http://dev.w3.org/html5/spec/Overview.html#custom-data-attribute, and they prove to be a much more elegant and robust solution to this problem. They’re custom, so they can contain pretty much anything you want, and each element can have as many of them as you want.

<ul class="people">
  <li>Paul</li>
  <li>Rebecca</li>
  <li>Alex</li>
  <li>Adam</li>
</ul>


var $portrait = $('#portrait');

$('ul.people li').click(function() { 
    var $li = $(this),
        name = $li.attr('data-name'),       
        color = $li.attr('data-hairColor'),
        $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');

    $portrait.append($img).css('border', '5px solid ' + color);
});

$.fn.data

When you want to embed information about an element in the HTML you send down from your server, custom data attributes offer a clear and easy solution. But what if you want to attach data to elements that you’ve added to the page using JavaScript? For example, you might have some data you fetched from your server via an Ajax request:

{
    "items" : [
        { "name" : "Paul", "image" : "paul", "hairColor" : "black" },
        { "name" : "Rebecca", "image" : "rebecca", "hairColor" : "brown" },
        { "name" : "Alex", "image" : "alex", "hairColor" : "red" },
        { "name" : "Adam", "image" : "adam", "hairColor" : "red" }
    ]
}

You’re going to iterate over the data to produce a structure much like the one above, but in this case, it doesn’t make sense to store the related data in markup, because you’ll just have to extract it again later. Instead, you can use the $.fn.data() method in jQuery to store the data using JavaScript instead of markup:

var $target = $('ul.people');

$.each(response.items, function(i, data) {
    $('<li/>', { html : data.name })        
      .data({  
        name : data.image,              
        hairColor: data.hairColor       
      })        
      .appendTo($target); 
});

Later, you can read the data off the element using the $.fn.data() method again, this time passing just the name of the key you’re after:

var $portrait = $('#portrait');

$('ul.people li').click(function() { 
    var $li = $(this),
        name = $li.data('name'),
        color = $li.data('hairColor'),
        $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');

    $portrait.append($img).css('border', '5px solid ' + color);
});

Mixing the two methods

This is all well and good, but what if you have a list of people that was sent from the server using HTML and custom data attributes, and then you add elements to it later using JavaScript and store data on them using $.fn.data()? Now your data is stored on elements in two different ways, so how do you extract it reliably? One option is to handle both cases. First, you’ll switch to using jQuery’s delegate method for the event binding, so you don’t have to keep binding click handlers as you add list items to your list. Then, inside of your click handler, you’ll figure out where you can get your data from:

var $portrait = $('#portrait');

$('ul.people').delegate('li', 'click', function() { 
    var $li = $(this), 
        name = $li.attr('data-name'), 
        color, $img;

    if (!name) { // did the li have custom data attributes?
        name = $li.data('name');
        color = $li.data('hairColor');
    } else {
        color = $li.attr('data-hairColor');
    }

    $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');
    $portrait.append($img).css('border', '5px solid ' + color);
});

Another option is to iterate over the original elements, and store the data from the custom data attributes using the $.fn.data() method:

var $portrait = $('#portrait'), 
      $ul = $('ul.people');

$ul.find('li').each(function() {
    var $li = $(this);
    $li.data({
        name : $li.attr('data-name'),
        hairColor : $li.attr('data-hairColor')
    });
});

$('ul.people').delegate('li', 'click', function() { 
    var $li = $(this), 
        name = $li.data('name'),
        color = $li.data('hairColor'),
        $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');

    $portrait.append($img).css('border', '5px solid ' + color);
});

// load more list items via ajax at some point
// to make that whole delegate thing worthwhile

Which option you use will depend on how large your original list is (and thus how long it will take to iterate over it), and how likely people are to click on a lot of items in the list (and thus whether the initial iteration is worth the time). I leave it as an exercise for the reader to decide which approach makes sense for you.

In Conclusion

When you need to attach data to elements and then extract that data later, there are options beyond classes and IDs, and in fact classes and IDs may be an especially poor way to approach the problem. Taking advantage of custom data attributes and the $.fn.data() method in jQuery can make it painless to store and retrieve data related to elements, and the metadata plugin can streamline the process for you even further.

Further Reading

Posted

8 comments

Jun 12, 2010
Colin Snover said...
I think it’s important to mention that HTML5 data attributes and $.fn.data both have slightly different use-cases, despite seeming the same on the surface.

HTML5 data attributes are incredibly useful when you need to query the DOM for custom data, or when you need some data to be immediately available at runtime, but they lack any sort of typing—that is, everything you put into an HTML5 data attribute is going to come back to you as a string.

Conversely, data stored in $.fn.data can’t be easily queried against, but it does allow you to store typed data like numbers and booleans, Function and Object references, and so on.

Some people try wedging JSON data into HTML5 data attributes to get typing, but that just adds extra parsing and library overhead that’s not really necessary—and it’s still impossible for them to handle complex Object or Function references.

So, I guess what I am saying is, use HTML5 data attributes for stuff you need to query against, and use $.fn.data for the rest, and you will be hapy!

Jun 12, 2010
Rebecca said...
@snover this reminds me that I should have pointed out that $.fn.data() is great for storing stuff other than strings -- objects, arrays, even other jQuery objects! So if clicking on an element should have some effect on another element, one can store a pointer to the affected element on the clicked element using $.fn.data(), saving the expense of tracking down the affected element every time the other element is clicked.
Jun 13, 2010
Alex Sexton said...
I think you hit the nail on the head, especially the clarifications in the comments too!

Mostly, though, I just wanted to thank you for putting me in your unordered list!

<3z

Jun 14, 2010
Things I Found Interesting Around June 13th | Chris Coyier said...
[...] jQuery: Storing and retrieving data related to elements [...]
Jun 16, 2010
Ricardo Rodrigues said...
Very good article, the data function is, most of the times, forgotten. We start shoving custom attributes into our element and when we look at it, our element is filled with custom attributes that should have been an object stored with the data function.
Jun 16, 2010
Brian Arnold said...
I'm noticing in that last example, you stash a reference to $('ul.people') in $ul, but don't use it again for the delegate call.

Also, thinking about that delegate section -- you create $li to get a couple of pieces of data using $.fn.data, but couldn't you use $.data and avoid the overhead of creating the $li object? I've been kind of on a kick with balancing out performance and convenience, and using $.data instead of $.fn.data has been one I've liked. Not that performance is necessarily an important thing in this example, and it might be a bit of premature optimization, but I like the look of something like this a bit more:

$ul.delegate('li', 'click', function() {
var name = $.data(this, 'name'),
color = $.data(this, 'hairColor'),
img = document.createElement('img');

img.src = '/images/people/' + name + '.jpg';
$portrait.append(img).css('border', '5px solid ' + color);
});

Cuts out the creation of three jQuery objects -- since you're just setting the source of the image and no alt values or anything. :) Again though, that's one of the nice things about jQuery, in that you can use it how it feels right.

This is a great blog post! jQuery's data system is super convenient and more people need to know about it, especially for storing more complex data structures. :)

Jun 16, 2010
Brian Arnold said...
Oh, another quick thought: If you want to stash that data from an Ajax call, in the $.each over response.items above, since you're basically recreating the object, seems like you might just want to stash .data(data) directly rather than rebuild? I mean, if you only want select attributes out of the data item, sure, but in this particular case, it looks like you'd get a bit more out of it, since you also have image that wasn't really being used. :)
Jun 16, 2010
Rebecca said...
Re $.data vs $.fn.data, I always struggle with whether to get into performance in examples that are trying to demonstrate a thing. I opted to go with the more jQuery-y way in this case to try to focus on the point I was trying to make, but you do make several good performance points that would be well worth considering in production code.

Re stashing the data from the Ajax response, you are totally right! I will fix it in the post, my bad.

Thanks for the comment :)

Leave a comment...