jQuery: Embedding Behavior

The fellows over at Err have finally seen the light of jQuery. My first thought is welcome to the party, we've been wondering when you'd get here. From my first days with rails, I've been using jQuery over Prototype. Why? Well, a lot of reasons, but the biggest one is because my rails mentor is Yehuda Katz, of Visual jQuery and jQuery in Action. From the first time we met, Yehuda echoed to me exactly what he says in the Err post comment thread.

jQuery is a perfect companion to Rails; most people who do any kind of non-trivial JS end up writing custom stuff anyway, and the built-in Rails helpers end up being more trouble than they’re worth.

Needless to say, I agree. In honor of the Chris and PJ jumping to the jQuery camp, I thought I'd answer a thoughtful question in the comments from partage. I know this is a Ruby blog, but hey, jQuery also saved my life.

I’ve also enjoyed mixing jQuery with rails; however, I wonder what techniques people are using to keep the javascript unobtrusive in div replacement scenarios?

To my way of thinking, it sometimes seems natural to package a chunk of markup with the behaviors it needs to operate properly. It’s particularly true when the markup operates as self contained component which may be used several places (for instance, via both partial includes and xhr div replacement).

In those situations, I prefer to call the behavioral javascript at the bottom of the markup rather than trying to ensure I add the behaviors externally (and repeatedly) on all page loads, xhr success handlers, etc. However, I usually suffer a few pangs of guilt with respect to ‘unobtrusive’ javascript.

If I understand correctly, the problem is related to the jQuery methodology of binding events on document ready.

Basic Event Binding

The key to remaning unobtrusive with jQuery is to:

  1. Select a piece of DOM, i.e $("a.update")
  2. Bind something to it, i.e $("a.update").click(function(){
    $(this).parent()
    .load('/posts/'+this.href.split("/").slice(-1)[0].match(/^\d+/))})

But how and when does this code actually run and get applied to the dom? Well, when the document is ready, as so:

 
$(function() {
  $("a.update").click(function(){
    $(this).parent().load('/posts/'+this.href.split("/").slice(-1)[0].match(/^\d+/));
  });
};
 

Simple, when the document is ready, $(function() {, select all div's with reload, and on click (an event binding), run a function that will load html from an ajax request into the dom that called, the function, represented by $(this). From what URL? From /posts/numbers-in-the-id-of-this-div. What's that mean? Well, I like to do this:

 
for p in @posts
  "
<div id=\"post-#{p.id}\" class=\"reload\">#{p.body}</div>
 
"
end
 

So when you click on that div, it will call /posts/id, which fits in nicely with rails, via ajax. It will take the result of that request, an replace the html of that div with it. Awesome. But if you click on that div again, nothing happens. That's because the dom has been replaced and the onclick event that was bound on document ready, no longer exists. Something must leave instructions for jQuery on what to do next, something must repass behavior instructions.

Talking Back to jQuery

In the example above, reloading the div's dom is a trivial feat - as in, you'd really want to use what I'm about to describe for a more complicated situation - for example, if you load a lightbox that has a form in it and which should be submitted via ajax and if validation fails in rails, should display errors and allow for resubmission until success, when it should display a confirmation, close the lightbox, and update some dom. However, I'm going to continue with the reload example because it's just easier to use, even if it's overkill.

Callbacks and Simple Rebinds

The first way to talk back to jQuery, to have the dom reinstruct the script with post-processing, is to use the many opportunities for callbacks and to simply use logic to rebind things. Now this really only works on the simpler examples, but it's the first option.

 
$.fn.reload_post = function(){
  $(this).click(function(){
    $(this).load('/posts/'+$(this).attr("id").match(/^\d+/));
    $(this).reload_post();
  });
}:
 

The problem with this method is that it's limited in complexity and basically requires a custom function for each implementation - as in that might work for reload_post, but what about reload_comment? But still, that's one options for binding behavior.

The Tempting but Bad Meta Data Idea

There is a great, well great in theory, poor in practice, plugin for jQuery called MetaData. What it basically allows you to do is embed meta data into DOM. At first, this is all I used for binding complex behavior to elements. So I'd have a lot of this in my code:

 
<a href="/products/share/1" class="save simple_post {success: NewSavedProduct, dataType: 'json', error: ErrorSimplePost}">
<a href="/products/share/2" class="share simple_post {success: ShareProduct, dataType: 'text', error: ErrorSimplePost}">
 

At firs,t I thought this was great. I could reuse the same JS function (simple_post - which just creates a POST request from the url of a link), but make it do different things by embedding the options I wanted into the element itself. After about two weeks, I hated it. In the end, it still wasn't flexible enough for me to give complicated instructions, I wasn't reusing that much code as ShareProduct and NewProduct were really custom functions, and in the end, I had totally compromised the semantic nature of my dom. So I nixed that and needed my own solution. Basically what I wanted were the metadata type options, just out of my dom, so I could reuse functions, load elements with tons of behavior information, but have no trace of that in my source.

An External Meta File

 
// Button Meta Data
var simple_post = {
	save_post: {
		{success: NewSavedProduct, dataType: 'json', error: ErrorSimplePost}
	},
	share_post: {
		{success: ShareProduct, dataType: 'text', error: ErrorSimplePost}
	}
};
 

The simple_post function can now use the data stored in this JSON object to instruct itself on what to do based on the link clicked. I've been using this method a great deal for the submit buttons on Designer Pages. There is one function that is bound to all the buttons and that calls a similar type of JSON object to find the instructions for the particular button clicked. This way when I want to add a new behavior for a button, I can just go into that meta.js file and add a new definition. It also integrates nicely with rails helpers. I think there is a lot of potential in this method so I'm going to save the gritty details for another post.

I hope this is a good food-for-thought piece on ways to embed behaviors for your elements in jQuery. In the next week I'll be introducing my first rails plugin is_popular, so subscribe.

Post a Comment

Your email is never published nor shared. Required fields are marked *