Sep
2009
A question came up yesterday on the jQuery mailing list where somebody wanted to effectively "embed" data into a link. They essentially wanted to call a function on click, but needed to pass arguments. Since jQuery is supposed to be unobtrusive (e.g. "Find Something, Do Something"), how can we tell jQuery that, when a specific link is clicked, there's specific data associated with that link?
The jQuery data() method allows you to do just that.
Here's a quick demo that illustrates how the data() method is used. There are no AJAX calls. The data is populated via a call to a CFC that returns a ColdFusion query object. All of the relevant data for a particular player is stored in a specific namespace. Code below.
First, a straightforward call to a CFC to obtain our data:
<cfset data = createObject('component', 'data') />
<cfset players = data.getPlayers() />
Which results in the data shown below:

Nothing new or different yet. In fact, let's flesh the page out a little bit more in the way that would be familiar to ColdFusion developers:
<cfset data = createObject('component', 'data') />
<cfset players = data.getPlayers() />
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title></title>
<style type="text/css">
#playerList, #playerInfo { float:left; width:160px; }
#playerList { width:160px; }
#playerInfo { width:400px; }
#playerInfo img { float:left; padding-right:8px; }
</style>
</head>
<body>
<div id="playerList">
<cfoutput query="players">
<a href="##" id="player#playerID#">#playerName#</a><br />
</cfoutput>
</div>
<div id="playerInfo"></div>
</body>
</html>
Easy! We've got a list of players, retrieved from our CFC. Now we want to add some pop, so we're going to use jQuery to listen for a click on one of the player name links, and then populate the (currently empty) "playerInfo" div with information about the player. To emphasize the beauty of jQuery's unobtrusive nature, we won't be touching the existing markup at all. Simply adding some script.
The first thing to do, of course, is to include the jQuery library. You can either refer to it on your own server, or the hosted version on Google's servers. I'm doing the latter.
<script src="http://code.jquery.com/jquery-latest.js"></script>
If you scroll up and look at the code example where we're outputting the links, you'll see that each link has a unique ID value of "playerN", where "N" is the playerID value returned from our query. We're going to assign our data to this same namespace.
<script>
$(document).ready(function() {
<cfoutput query="players">
$('##player#playerID#').data('playerInfo', { number:#playerNumber#, position:'#playerPosition#', image:'#PlayerImg#', college:'#PlayerCollege#' });
</cfoutput>
});
</script>
Inside our $(document).ready(), which fires automatically when the page is finished loading, I do a simple query-driven <cfoutput>. It might be easier to follow along if you see the actual JavaScript that is generated:
$('#player1').data('playerInfo', { number:13, position:'WR', image:'http://assets.giants.com/uploads/players/2FE2D3BDF4FB443D949D1D39B69ADC03.gif', college:'Cal Poly' });
$('#player2').data('playerInfo', { number:47, position:'TE', image:'http://assets.giants.com/uploads/players/7BD58CBD8C6844DE875964C1B06A4757.gif', college:'Wisconson' });
$('#player3').data('playerInfo', { number:95, position:'DT', image:'http://assets.giants.com/uploads/players/63E56B04414F4677B13B8314A3C3A6E9.gif', college:'Texas A&M' });
$('#player4').data('playerInfo', { number:89, position:'TE', image:'http://assets.giants.com/uploads/players/B82703AB4B5F416BAEE1CD35F4F49810.gif', college:'Western Oregon' });
$('#player5').data('playerInfo', { number:44, position:'RB', image:'http://assets.giants.com/uploads/players/003E58E09A764087802C550C738C2397.gif', college:'Marshall' });
$('#player6').data('playerInfo', { number:18, position:'P', image:'http://assets.giants.com/uploads/players/E4D8C7C767D74D31BE13F2042CDA5948.gif', college:'Miami (FL)' });
$('#player7').data('playerInfo', { number:27, position:'RB', image:'http://assets.giants.com/uploads/players/DDDE77B88074439FA9C7DC9D1C5F6289.gif', college:'Southern Illinois' });
$('#player8').data('playerInfo', { number:10, position:'QB', image:'http://assets.giants.com/uploads/players/1D2BC758565448988AB8BCB25AF580A2.gif', college:'Mississippi' });
$('#player9').data('playerInfo', { number:82, position:'WR', image:'http://assets.giants.com/uploads/players/BF00DD399A1348E481E050330306DBA6.gif', college:'Michigan' });
$('#player10').data('playerInfo', { number:91, position:'DE', image:'http://assets.giants.com/uploads/players/AA3798254BD444BFB7BC247FC4C43A11.gif', college:'Notre Dame' });
For each iteration of our output, we're assigning data (using the data() method) to the element with ID "playerN" (again, "N" representing the playerID returned from the query). The data is going to be assigned to a variable named "playerInfo". The value of the "playerInfo" variable is a JSON string containing the number, position, image, and college of each of the players. All of these values were populated from our cfquery data.
Now that we've got this data, let's get set up for displaying it.
We know that we have our links, each with ID "playerN". So we're going to want to listen for a click event on an "a" element that has an ID attribute that starts with "player". We can do this with a jQuery selector and attribute filter:
$('a[id^=player]').click(function() { (we're going to do stuff here) });
To break down that selector a bit... if we wanted to select every "a" element on the page, we'd simply do:
$('a')
The attribute filter allows us to filter those elements by a given attribute. If we wanted every "a" element that has an ID attribute, we'd do:
$('a[id]')
If we wanted to filter for all "a" elements that have an ID attribute of "player", we'd do:
$('a[id=player]')
Because we only want "a" elements that have an ID attribute that begins with player, we end up with:
$('a[id^=player]')
Notice the caret (^) character. This is what specifies to jQuery that we don't want an ID with the exact value of "player", but rather one that starts with the value "player", which is exactly what we need.
Now that we've sufficiently established that we're capturing the correct click event... what do we do inside of it?
var playerData = "";
playerData += '<img src="' + $(this).data('playerInfo').image + '" />';
playerData += "Number: " + $(this).data('playerInfo').number + "<br />";
playerData += "Position: " + $(this).data('playerInfo').position + "<br />";
playerData += "College: " + $(this).data('playerInfo').college;
First line creates a variable called "playerData". Then with each subsequent line, I'm simply appending text to that variable. Most of it should be fairly straightforward string concatenation in JavScript. Literal values go in quotes, variables (JavaScript variables) outside of quotes, and a plus sign to join them together.
What should be new to you is the specific JavaScript variables. $(this) refers to the element that triggered the click event. $(this).data() is a hook into the data that we had previously associated with that element. We want to retrieve the "playerInfo" data, so we reference that specifically via $(this).data('playerInfo'). The data we stored in that variable was JSON. Think of JSON as a string representation of a ColdFusion structure. It's got keys, and it's got values. The "keys" that we set were "image", "number", "position", and "college". Therefore, on the appropriate lines, those are the keys we're going to retrieve.
We should now have a bit of HTML stored in our playerData variable. The final step is to insert it into the page.
$('#playerInfo').html(playerData);
return false;
Again, using a jQuery selector, we identify the element with the ID value of "playerInfo". This is an empty div that we created on the page. Using jQuery's built in html() function, we're going to set the html by passing in the playerData variable (which was the HTML string we previously constructed). Finally, we return false to prevent the browser from attempting to follow the link... and we're done.
The final markup:
<cfset data = createObject('component', 'data') />
<cfset players = data.getPlayers() />
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title></title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
$(document).ready(function() {
<cfoutput query="players">
$('##player#playerID#').data('playerInfo', { number:#playerNumber#, position:'#playerPosition#', image:'#PlayerImg#', college:'#PlayerCollege#' });
</cfoutput>
$('a[id^=player]').click(function() {
var playerData = "";
playerData += '<img src="' + $(this).data('playerInfo').image + '" />';
playerData += "Number: " + $(this).data('playerInfo').number + "<br />";
playerData += "Position: " + $(this).data('playerInfo').position + "<br />";
playerData += "College: " + $(this).data('playerInfo').college;
$('#playerInfo').html(playerData);
return false;
});
});
</script>
<style type="text/css">
#playerList, #playerInfo { float:left; width:160px; }
#playerList { width:160px; }
#playerInfo { width:400px; }
#playerInfo img { float:left; padding-right:8px; }
</style>
</head>
<body>
<div id="playerList">
<cfoutput query="players">
<a href="##" id="player#playerID#">#playerName#</a><br />
</cfoutput>
</div>
<div id="playerInfo"></div>
</body>
</html>
You can copy/paste and run locally as long as you have a CFC named "data.cfc" in the same directory, and that CFC has a method called getPlayers() that returns a query with columns playerID, playerNumber, playerName, playerPosition, playerImg, and playerCollege.


Comments
Add Comment | Subscribe to Comments
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Add Comment# Posted Steve C. on 9/23/09 3:57 PM
This is anything but unobtrusive. You're outputting a ton of inline javascript in script tags. That's almost as bad as assigning a click to each and every function inline on the element.
A slightly lesser evil would be to use an expando, but your html won't validate.
What we really need here is a good example of how to use an element's data when having to output values from the server side, but WITHOUT using inline scripts. I have yet to see a good example of this anywhere.
# Posted Charlie Griefer on 9/23/09 4:05 PM
@Steve - Given what's available right now, I'm not sure how it can get much more unobtrusive than this?
By "unobtrusive", I'm referring specifically to the fact that the JavaScript is completely de-coupled from the markup.
I don't understand how using JavaScript in <script> tags is a "bad thing"? I mean, assuming one actually wants to use JavaScript, what other option is there? Further, what other option is there that's less obtrusive?
# Posted Derek P. on 9/23/09 4:11 PM
@Steve C.
Yes, this not unobtrusive. No, this is not as bad as inline assignments. are you kidding me?
Unobtrusive roughly means deprecated experiences for non-javascript enabled interactions, and layers of behavioral separation. That however doesn't mean that inline script tags are bad!
As for the example you are looking for, any 101 functional programming technique could be applied to element data, what do you need to see?
@Charlie,
I am really excited about this style of integration of data into presentation layers, it is really useful especially for situations where you want the presentation to drive logic based on more complicated scenarios.
# Posted Steve C. on 9/23/09 4:19 PM
I'm aware of what unobtrusive is. Having the server side output a large amount of inline javascript inside a script tag is going to grow your page significantly. What happens if your table is large? As it stands right now you have rendered one call for every row. This doesn't scale, and adds to your page weight.
Though my intention wasn't to come across as overly critical, it just belies my frustration with the existing usages of the data function for this type of thing.
# Posted Charlie Griefer on 9/23/09 4:27 PM
@Steve - And my intention wasn't to come across as condescending. We very well might have had different definitions of what constitutes "obtrusive". I'm not convinced that we don't. Your contention is that this method could result in large page weights (it could). I don't consider that so much obtrusive as bad planning (using the wrong tool for the wrong job).
If there's a large amount of data retrieved from the server? Then I'd look at using an AJAX-based approach and returning only what's needed for a given user interaction.
Please don't lose sight of the fact that this is meant to be an example to illustrate how to use the data() method *if* the data method is an appropriate solution to your problem. I'm not suggesting that it should be used in all circumstances.
# Posted Don on 9/23/09 4:33 PM
One of the drawbacks of this approach is that you have to download the data for all players whether the user wants to view it or not. That certainly wouldn't be efficient if you had a large list.
I think I would approach it by having the link retrieve that player's data from a cfc which then populates your playerinfo div.
# Posted Charlie Griefer on 9/23/09 4:36 PM
@Don - Yes. Please see my response to @Steve C above :)
Maybe tomorrow I'll do a follow-up showing how to use getJSON() to retrieve requested records via AJAX :)
# Posted Alex on 9/23/09 5:55 PM
The way I've overcome problems like this is to put JSON data in an HTML comment block. It's a LOT more compact than this, and a lot easier to output on the server side.
# Posted ajpiano on 9/23/09 8:28 PM
This is really not a technique I would recommend in the slightest. You could get a lot more mileage out of converting a query to a structure and using the SerializeJSON function to store it as metadata on the element, which you could retrieve with the jQuery metadata plugin. I use this approach all the time, and it achieves more or less the same effect, without using CF to *generate* a bunch of JavaScript on the fly, which is nasty in whatever language you do it in, IMO
# Posted ajpiano on 9/23/09 8:29 PM
Also, of all searches, ^= is really not the most efficient. Better to use a class and id so you have the class search available.
# Posted Charlie Griefer on 9/23/09 8:50 PM
@AJ - Thanks for the comment. I'm not familiar with the Metadata plugin. I just took a quick look at it and it seems that at first glance it'd be more obtrusive. Yes, this method loops over a CF query and generates 10 lines of JavaScript (one for each record returned), but it's not particularly complex. And in the end, the JavaScript is still de-coupled from the HTML, which I like a lot.
From what I can see of the metadata plugin, it becomes pretty tightly coupled with the markup by way of storing the JSON in the class attribute (or creating a bogus attribute... which would affect validation).
I'll take a closer look at the Metadata plugin, but it seems to me there's pros and cons to both approaches (as is usually the case).
Appreciate the input on the selector, too. I'd never thought about using both a class and an ID as attribute filters. I can certainly see where that would limit the amount of searching that jQuery has to do.
# Posted Brian Swartzfager on 9/24/09 5:05 AM
One thing you need to watch out for when using this query output technique (with the data() method or with the metadata plugin) is the potential presence of single or double-quotes in the data. If you output a school name like "St Mary's", the college property assignment will break.
That can be solved by replacing all single-quotes with '\ and all double-quotes with " ("ampersand quot;" as it'll probably be escaped when the comment is displayed) as you loop through the data.
I'm a fan of both the data() method and the metadata plugin. The jQuery plugin I'm currently working on wouldn't be possible without the data() method.
# Posted ajpiano on 9/24/09 6:55 AM
Since the "metadata" is element specific, I think it makes a lot more sense to store that information on the element and use .metadata() to parse it. If you want it to validate, you can use HTML5 data attributes for metadata as well. My philosophical objection, again, is to the generation of JS on the fly, which leads to a situation where your JS doesn't really exist from a conceptual standpoint. The generated functions don't look DRY and are hard to digest, and you've filled your behavioural JS up with a tonne of data. The metadata plugin links the object right into the .data() cache anyhow, though Brian's caveat about the single quotes still applies.
It depends a lot on your idea of "coupling." The bottom line, which Steve seems to suggest, is that there shouldn't be a one-to-one relationship between the number of elements on your page and the amount of code you have in your JS. Elements that are identical except for their content should share the same behaviour in JS, and the metadata plugin simply eschews the need for the ugly manual feeding of this data into JS. Yes, the weight ends up very similar, but at least your JS is closer to being a program and further away from being a kludgy mess of database output.
# Posted Charlie Griefer on 9/24/09 9:22 AM
@Brian - Good point. ColdFusion's got the jsStringFormat(), which I never remember to use until I've spent at least 30 minutes debugging a JavaScript error :)
@AJ - I do see your points about the metadata plugin, and while they may be subjectively good (or bad), it's a matter of preference. Whether it's wrong/right/good/bad to put the metadata in a class declaration or in a <script> block is entirely dependent on who you ask.
But I appreciate that you brought up the metadata plugin. I do intend to check it out, and I prefer that people know which options are available so they can choose the one that works best for them.
# Posted Dan G. Switzer, II on 9/25/09 11:40 AM
@Charlie:
I really don't recommend using the data() method for this--it's quite overkill and not very efficient. Instead I'd do something like (which is unobtrusive:)
HTML:
<div id="playerList">
<cfoutput query="players">
<a href="getprofile.cfm?id=#playerId#" id="player-#playerId#" class="player">#playerName#</a><br />
</cfoutput>
</div>
SCRIPT:
$(document).ready(function (){
// if running jQuery v1.3 or higher, use the "live" method instead--much faster initialization performence
$("a.player").click(function (){
// get the anchor clicked on
var $el = $(this);
// load the player's info
$('#playerInfo').load($el.attr("href") + "&ajax=true");
return false;
});
});
In this example, the getprofile.cfm would be a page for showing off the profile. If the user doesn't have JS enabled, it works perfectly. If the user has JS enabled, then we go to the same URL via an XHR call but pass in a variable that says "Hey, I'm an ajax call, just return to me the fragment of HTML you want me to display!"
You could also just grab back the data via JSON:
$(document).ready(function (){
// if running jQuery v1.3 or higher, use the "live" method instead--much faster initialization performence
$("a.player").click(function (){
// get the anchor clicked on
var $el = $(this)
// get the "playerId" for the user
, playerId = $el.attr("id").split("-")[1];
// I'm assuming there's a method called "getPlayer" which passed a playerId returns their info
$('#playerInfo').getJSON("data.cfc?returnFormat=JSON&method=getPlayer&playerId=" + playerId);
// now do the playerData code...
});
});
The beauty of this is because you're not loading all the data into the page at run time, it works for as many or as little records you have. In your original method if you had 1000s of records, you'd have a lot of performance issues, while this version would work great regardless of the number of records.
-Dan
# Posted Charlie Griefer on 9/25/09 12:04 PM
@Dan: You might have just saved me from writing another blog entry :)
I'm aware that getting 'n' records when you may only ever need 1 isn't particularly efficient (although I do remember back in the day, looping over entire CF query objects generating JS arrays for things like related selects... ugh).
This was really just meant to be a simple example of how the data() method works. Not a suggestion as to a best practice.
I was about to start a new entry on using getJSON(), which would be a better approach to suit this particular example. The load() example looks even better though, for the reason you state (graceful degradation). I'm going to have to take a closer look at that.
Thanks!