jQuery metaData() Plugin Leverages data() Method (jQuery Plugin Example)
When I first learned about the jQuery data() method, it totally blew my mind. The ability to store data associated with an element in a way that doesn't hack the XHTML was just brilliant! But, I feel that there is something just slightly lacking from the data() method - the ability to gather all the stored values as a collection. The way data() works now, you can either set the value of or get the value of a given key; if you have multiple values associated with a given element, you have to make multiple calls to the data() method to retreive them.
I figured it would be nice to have the option to pass the data() method no arguments and get back the entire set of associated data (in name-value pairs).
- 0 Arguments :: Get entire data set of first element.
- 1 Arguments :: Get given data point of first element.
- 2 Arguments :: Set given data point for entire collection.
To create this functionality, I built the metaData() plugin which wraps around jQuery's core data() method and adds the zero-argument option. This plugin was covered in my Intensive Exploration of jQuery presentation, but I thought it would be nice to cover it here as well as an example of how to build a jQuery plugin:
<script type="text/javascript">
// The meta data method will wrap around the data function,
// but use a single point of entry for our data so that we
// can get the key collection of no parameters are passed.
jQuery.fn.metaData = function(){
var strCollectionKey = "__metadata__";
var objData = null;
var strKey = "";
var strValue = "";
// Check to see how many arguments were passed-in. The
// number of arguments passed-in will determine the
// type of action to take:
// 0 :: Get entire data collection (first element).
// 1 :: Get data point (first element).
// 2 :: Set data point (entire collection).
if (arguments.length == 0){
// Return the data collection for the first element
// in the jQuery collection.
// Get the meta data collection. If this value has
// not been set yet, it will return NULL.
objData = this.data( strCollectionKey );
// Check to see if we have a data object yet.
if (!objData){
// The data store has been set yet. We want to
// set a value (even through we are going to
// return it) so that we know that we will
// always be referring to the same struct (by
// reference) in later calls.
this.data( strCollectionKey, {} );
}
// ASSERT: At this point, we know that a strcuture
// exists in our meta data key (even if it is empty).
// Return an meta data collection.
return( this.data( strCollectionKey ) );
} else if (arguments.length == 1){
// Return the value for the first element in the
// jQuery collection. To get the data collection, we
// will call the metaData() method recursively to
// get our store before we return the data point.
return(
this.metaData()[ arguments[ 0 ] ]
);
} else if (arguments.length == 2){
// Set the given name value. We will be doing this
// for each element in our jQuery collection.
// Create a local reference to the arguments (so that
// we can later refer to them in the each() method.
strKey = arguments[ 0 ];
strValue = arguments[ 1 ];
// Iterate over each element in the collection and
// update the data.
this.each(
function( intI, objElement ){
// Get a jquery reference to the element in
// our collection iteration.
var jElement = jQuery( objElement );
// Get the meta data collection. To get this,
// we will call the metaData() method recursively
// to get the store.
var objData = jElement.metaData();
// Set the value. Because the data store is
// passed by reference (why I made a point to
// set it before returning it above), we don't
// need to re-store it.
objData[ strKey ] = strValue;
}
);
// Return the current jQuery object for method
// chaining capabilities.
return( this );
}
}
</script>
What I really like about this plugin is that it not only leverages an existing part of the jQuery core library (the data() method), but that it calls itself recursively to handle the various argument options.
Now that we have our plugin defined, let's run a little test to see how it works:
<script type="text/javascript">
// Get the document element.
var jDoc = $( document );
// Store random values in the document using the
// metaData() method. Notice that each of these is
// a separate call.
jDoc
.metaData( "created", new Date() )
.metaData( "location", window.location.href )
.metaData( "userAgent", navigator.userAgent )
;
// Now that we have stored these values independently,
// get the collection and iterate over it.
$.each(
jDoc.metaData(),
function( strKey, strValue ){
document.write(
strKey + " : " +
strValue + "<br />"
);
}
);
</script>
As you can see in the demo, we are making three independent calls to the metaData() method to set data points. However, when we output the data, we make a call to metaData() with no arguments so as to get the entire collection. Running the above code, we get the following output:
created : Thu Mar 05 2009 14:25:01 GMT-0500 (Eastern Standard Time)
location : http://......./testing/jquery/meta_data.htm
userAgent : Mozilla/5.0 Gecko/2009011913 Firefox/3.0.6
While it's not that often that we need to output an entire collection of data within an application, I have found this plugin to be extremely useful when it comes to debugging jQuery scripts:
console.log( $( "...." ).metaData() );
jQuery is so awesome and so easy to extend! I love you jQuery.
Want to use code from this post? Check out the license.
Reader Comments
From what I understand, jQuery regularly takes community plugins and makes them part of the core library. This would probably be an easy in.
@David,
I tried to peek under the hood and see how they are actually performing the data() method lookups. While I couldn't follow it exactly, it looks like it is using custom event handlers and somehow storing the data as the event data.
I can say, but I think it would be easy to rework that and use one event rather than several events and build this in... just guessing though.
@Ben:
jQuery stores all of the data into the jQuery.cache key. You could just use the internal storage methods to get all the keys instead of wrapping up your own internal methods:
jQuery.fn.getAllData = function (){
// the jQuery.data(this[0]) should retrieve the cache key for an element
return jQuery.cache[ jQuery.data(this[0]) ];
}
I didn't test it, but a quick look of the source code this should be right. The $.data() function is supposed to return an element's cache key.
So, we're just returning the entire data cache with all keys.
@Dan,
Hmm. I could have sworn that it was using some sort of custom event to store and retrieve data. Of course, I find it a bit hard to follow the underlying source code, so I could be 100% off! I'll have to do a bit more digging. I would like to understand the way it's working.
@Dan,
I just took a look at the source code. It's a bit tough for me to follow, but it seems you are 100% correct. Looking at line 1274 of the 1.3.2 development version, I am seeing the jQuery.cache object. I don't see how exactly the $.data() method gets the data cache based on the element, but still, very awesome. You are correct, got the whole thing to work with this:
jQuery.fn.allData = function(){
. . . . var intID = jQuery.data( this.get( 0 ) );
. . . . // Return entire collection.
. . . . return( jQuery.cache[ intID ] );
}
@Ben:
And don't forget, if you mix in a closure with the apply() method from your earlier blog, you could just overwrite the default behavior:
// a closure
(function ($){
// make a copy of the original data method
$.fn._data = $.fn.data;
// extend data
$.fn.data = function (){
if( arguments.length ) return $.fn._data.apply(this, arguments);
else return jQuery.cache[jQuery.data( this.get( 0 ) )];
}
})(jquery)
Once again, not tested but the theory is sound.
However, you do always want to watch overriding functions in a library like this, since you never know when the internal methods might change or when your mod could break something else. This was more an example of what's possible.
@Dan,
I have spend the last 15 minutes scrolling over the jQuery library (shhhh, I should be working). I have never really used the syntax:
(function(){ ... })();
I see that their whole library was defined that way and that is how they used that "expando" variable to create UUIDs for the elements. Very cool stuff. I need to start experimenting that that methodology.
I agree that overriding could be dangerous... but you're not really overriding, you're more like alternately executing :) Very cool though! You have been a big help to me today on several posts!
After I posted my sample, I realized that I shouldn't have defined the copy of the $.fn.data() method in the $.fn namespace, but should have just used a local variable inside the closure.
The whole (function (){})() is just a JavaScript closure. Essentially you're creating a one time function that is immediately executed and whose variable definition is completely protected.
Take this example:
var x = "Dan"
alert(x);
(function (){
var x = "Ben";
alert(x);
})();
alert(x);
When you execute this code you'll get 3 alert boxes in a row that show up in this order:
"Dan"
"Ben"
"Dan"
The nice thing about closures is they garbage collect very nicely. Now closures aren't for everything, but they work very well when defining your own jQuery plug-ins because all your code is protect from mucking up the global namespace.
Here's an example of a closure in a real world example.
I was recently working on a legacy application that always requires a function called setPageSettings be available. This function is included in a library that's load on every page.
However, on some pages in the application, developers have already written a custom version of the function to handle logic on that specific page.
Since the global function we define on every page comes after the custom override, we need to make sure the browser only runs the code if the function doesn't exist.
In IE6 and Chrome, without the use of a closure, the following code doesn't work because it's in the global namespace.
/* only declare the setPageSettings function if it doesn't already exist */
if( !window.setPageSettings ){
/* we declare this in a closure, so IE6 and Chrome don't see the function unless we actually run the code */
(function (){
window.setPageSettings = function (f){
// logic here
}
})();
}
(FYI - On a completely non-related note, it doesn't seem like any of the checkboxes at the bottom of the comment entry are working for me (i.e. Remember my information, subscribe to entry, etc.) I'm not getting comments or the cookie set to remember my information.)
@Dan,
It's very cool stuff. I can see how it would be especially useful for creating jQuery plugins that need to have some code executed.
What browser are you using? I will see if I can get the checkboxes to work.
@Ben:
It's not holding for me in any browser. I've tried IE7 and FF3 under Vista x64. I would have thought it might be on my side (although I'm not having problems elsewhere) except for the fact that I'm not getting any e-mail notifications with the "Subscribe to entry" checked...
... testing in Safari... I had no cookies set before open this up. Now, checking all and seeing what happens.
Hmmm, works for me on Safari. Dan, does the old comment page still work for you:
www.bennadel.com/index.cfm?dax=blog:1518.comment
Test...
@Ben:
Yes, that worked fine. It's also now remember my information on this page.
Oops!
As soon as I submitted from the new page it erased my information.
@Dan,
Hmm. I am relying on the browser to pass the COOKIE back and forth in the AJAX post. Maybe for the AJAX post, I will just set the cookie using Javascript. We'll get to the bottom of this!
A test post... (I want to view the XHR request.)
A test post... (I want to view the XHR request.)
@Dan,
Oh right, and I was supposed to start setting cookies via Javascript. Let me look into that right now.
@Ben:
Just an update. I still believe the issue may be on the server side of things--only because the "Subscribe to entry" option isn't working at all for me. The value is completely ignored.
I just checked the XHR request, and here's the data that was posted on my last comment (which you can delete:)
dax=blog%3A1518.commentapi&author_name=Dan+G.+Switzer%2C+II&author_email=dswitzer%40pengoworks.com&author_url=http%3A%2F%2Fblog.pengoworks.com%2F&content=A+test+post...+(I+want+to+view+the+XHR+request.)&remember_info=1&subscribe=1&send_copy=undefined
As you can see, the remember_info and subscribe are being passed to the page.
I'm seeing this as the response:
{"ERRORS":[],"SUCCESS":true,"DATA":16018}
So everything appears to process fine. It also does the window.location after the AJAX prompt.
Anyway, I just wanted to let you know the details from my end. I just find is highly suspect that the "Subscribe" flag isn't being honored (which leads me to believe it's on the server end of things.)
@Dan,
Thanks for the feedback. I just added some client-side Javascript cookie handling (testing now).
I just changed the way the data is sent.
@Dan,
I have changed the AJAX to send boolean values rather than how it was working before. When you get a chance, would you mind testing.
@Ben:
Another test. Also, just as an FYI--since the "Subscribe" isn't working, you might want to just send me e-mail directly (since I just keep coming back to the page and checking for new comments when I remember.)
Good news! Whatever changes you made, seem to have worked. At least the changes were remembered on last post. This is just another test. If I don't post again, you can assume all is well.
@Dan,
Awesome man, thanks for your help. It came down to the way the checkboxes were being sent. Originally, they were sent as "undefined" if they were not checked. I have since updated them to send true/false as far as checked.
Thanks for helping me debug.
Ben,
I've been searching my brains out on Google trying to find out how to extend an existing jQuery plugin with new, unanticipated functionality and I have to admit it has been a frustrating experience.
This blog entry helps in some respects but doesn't quite clear it up for me.
I am trying to create a new plugin called 'sticky' that extends (inherits from?) jQuery UI dialog with one simple function...it alters (overrides?) the drag event to make the dialog visible and its relative position regardless of scrolling the page.
I think I need to save some data in the new plugin and I was thinking of putting in the new data area of each dialog selected as you've shown here. But I'm clueless as to how to override the drag event.
Any ideas / suggestions on how I would approach this?
@Marv,
I believe the jQuery UI dialog widget already is sticky? In my experience, it will always pop up in the center (or wherever you define it to) regardless of the current page scroll.
Ben,
Whilst working on a project late in the night, I was possessed by not-quite-totally-on-topic urge to figure out more about .data().
I read your metadata() post, and was truly impressed with the effort and thought that went into digging under the hood of jquery to build .metadata().
But I had the nagging thought that there must be more to .data() than key/value pairs--and the jquery doc states that anything, not just simple values, can be stored--so I kept poking around until I found some examples of the techniques that can be used to access complex data structures stored in a .data() key.
And when I found them, I had the approximately 10,000th head-smacking "ooooh...it all makes perfect sense now that I've seen it" moment since I started computing (too) many years ago.
So, I'd like to share the following example of using .data() to create a collection within a key, assign key/value pairs, and then iterate over it:
//create data key with empty object
$(document).data('foo',{}) ;
//create shorthand access to the data key
var $dat = $(document).data('foo') ;
//assign arbitrary properties to the object in the data store
$dat.id = "fred" ;
$dat.status = "frenemy" ;
$dat.plans = "pie in face" ;
//iterate over the collection
var review = '' ;
for (key in $dat) {
review += key + ': ' + $dat[key] + '\n' ;
}
alert(review) ;
Thanks for everything that you do. If I tried to contribute as much as you do to the general pool, I'd never get *any* work done.
@Michael,
Good example, that makes a lot of sense. Essentially, that is what I'm trying to do with the metaData() plugin. However, your example is nice in that it demonstrates that we can get the benefits of the metaData() plugin while still being able to group our collections of data within their own namespaces.