Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
more page dynamics, less client logic

Ajaxed pages frequently need to update many parts of the page within ajax callbacks. Here I’ll outline how we use jQuery and Rails do this on Mavenlink. Our approach encodes behavior declaratively in markup and minimizes client-side logic.

Mavenlink provides a rich collaborative workspace for clients and consultants. All workspace activity happens within a single page. When a user adds a new deliverable to the workspace, we need to update three parts of the page. The dynamic page areas are highlighted in this screenshot of a typical workspace:

Mavenlink workspace

See the page in action yourself by creating your own workspace on Mavenlink.

To add a deliverable, the user submits the ajaxed form circled at right. Upon success, we need to update the deliverable module (A), the list of deliverables in the popup (B), and the event feed (C).

However, we’d like to avoid having our javascript know this. We try to avoid callback code along the lines of “when X happens, do A, B, and C”. Additionally, we want to avoid creating multiple event listeners responsible for dealing with something like a “deliverableCreated” event.

Instead, we mark up the parts of our document that can be updated dynamically. When new content arrives via ajax, we match the new content to the marked up dynamic elements, using shared attribute values to perform the match.

Specifically, we define a custom attribute called ‘content-key’ on our dynamic elements. Here’s three sections of the page’s original markup defining the three elements outlined in the screen shot:

<!-- deliverable module (A) -->
<div content-key="deliverable-module">
  ...
</div>

<!-- the deliverable popup (B) -->
<ul content-key="deliverable-popup">
  ...
</ul>

<!-- the event feed (C) -->
<ul content-key="events">
  ...
</ul>

In handling an ajax request to create a deliverable, we generate response markup with matching elements. The response looks something like this:

<div content-key="deliverable-module">
 <!-- new html -->
</div>
<ul content-key="deliverable-popup">
 <!-- new html -->
</ul>
<ul content-key="events">
 <!-- new html -->
</ul>

Our ajax success callback invokes a function that uses jQuery to inject the new html into the page’s original content-key elements:

function updateContent(event, newContent) {
  var $contentKeyElements = $(newContent).filter('[content-key]');

  // iterate through each new content-key element

  $contentKeyElements.each(function() {
    var contentKey = $(this).attr("content-key");

    // replace existing content-key html with new content-key html

    $("[content-key="+contentKey+"]").html($(this).html());
  });

  $(document).trigger('content-updated');
}

One advantage of this approach is that we can add new behavior without adding client-side logic. For example, if we want to show a flash message when a deliverable is created, we add a <div content-key='flash-notice'> element to the page, and match it with markup returned in the ajax response. The new behavior is encoded declaratively in our markup.

This approach keeps the server side pretty simple as well. In our create() action, we render the same partial whether the xhr request succeeds or fails:

class DeliverablesController < ApplicationController
  ...

  def create
    @deliverable = @workspace.deliverables.build(params[:deliverable])
    status = @deliverable.save ? 200 : 400
    render :partial => 'deliverables/deliverable_response', :status => status
  end
end

The deliverable_response partial renders secondary partials that each render a content-key element reflecting the new state of the page:

<%= render :partial => 'deliverables/deliverables_module' %>
<%= render :partial => 'workspaces/deliverable_popup' %>
<%= render :partial => 'workspaces/events' %>

These secondary partials are the same ones used to build the original page, so we’re not writing any new rendering code for ajax. Validation error messages are rendered within deliverables/deliverables_module in the manner of normal Rails forms.

You may have noticed that in our ajax responses, we render more than we strictly need to. For example, when a deliverable is created, we could just add a single new <li> element within our <ul>s instead of rebuilding the <ul>s entirely. For moderately-sized, moderately dynamic pages like our workspace page, micro-optimizing responses can needlessly complicate code without providing any meaningful performance benefits. When reality permits, we prefer blunt stupidity over intricate precision. It’s much easier to deal with.

Watch out for replacing markup that carries dynamic state, such as tab selection and expand/collapse state, both of which we use on Mavenlink’s workspace page. You can either avoid destroying state by narrowing the scope of your content-key elements, or restore the state in javascript after new markup is loaded. In a later post, I’ll detail how we deal with it on the workspace page.

Comments
  1. Josh Susser says:

    That’s pretty sweet. One suggestion though… You could use an HTML5 custom data attribute data-content-key, so all that will be well supported in future browsers, and someday it will even validate.

    http://dev.w3.org/html5/spec/Overview.html#custom-data-attribute
    http://ejohn.org/blog/html-5-data-attributes/

  2. JB Steadman says:

    Indeed, HTML 5 doctype + “data-*’ is your friend if you want to validate with custom attributes.

  3. Ryan Carver says:

    We use a similar technique, but with +id+ and +rel+ attributes for content insertion. I like the idea of data-* attributes since +id+ can be used in so many places.

  4. You could checksum the objects for each piece of the partial and pass that as an attribute in the root html of each module. When the new data comes from the server, compare the checksums against the existing client-side attribute and skip the dom/page refresh unless necessary.

    Since the processing would be server-side there wouldn’t be any negative client side cost.

    -matt

  5. Dave Smith says:

    Nice work, JB. How’d you go about testing it on the client side? Selenium?

  6. JB Steadman says:

    @Matt – nice optimization idea, thanks!

    @Dave – we unit test our ajax callbacks with Jasmine, and supplement with a minimal suite of coarse-grained Selenium tests.

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *