jQuery: Storing and retrieving data related to elements
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 worthwhileWhich 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
8 comments
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!
$.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.
Mostly, though, I just wanted to thank you for putting me in your unordered list!
<3z
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. :)
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 :)