MetaSkills.net

The "AJAX Head" Design Pattern

Posted On: May 24th, 2008 by kencollins

This is the first of a few articles covering the total rewrite of the HomeMarks.com code base as I upgrade it to Rails 2.1. The "AJAX Head" pattern is the moniker I have assigned to methodology that has come about during said rewrite and the design decisions I choose early on. The rules were simple.

  • Total Unobtrusive JavaScript (No Tag Soup)
  • No RJS Controller Responses

Rails makes it very easy to generate JavaScript with Ruby. The generator methods are extensive and cover both the Prototype and Scriptaculous libraries. The first version of HomeMarks used the generators exclusively to augment both the views and controller responses. In fact, I used a technique that is covered in the latest Advanced Rails Recipes (way before it came out) under the "Replace In-View Raw JavaScript" chapter. Basically I created helper methods that generated JavaScript shared by both the views and controllers. It was a great technique but things got messy quick. So life was good back then, I had a lot of Ruby code and a very dynamic site, but something did not seem right...

What Is The AJAX Head Pattern?

It is an experiment into a vision to see what happens when you make the decision to totally go unobtrusive. Not just in your views but the controllers too! Imagine it this way — your controllers are slim APIs. They should do nothing but render a page on a GET request and when it comes to a POST/PUT/DELETE they should do nothing more than just say YES or NO (with errors).

Now lets think about that. What does that mean for an application that uses exclusive AJAX calls for dynamic page updates? It means that your visual client (the browser) will need to know ALOT less from the controller about what to do. It's great, the browser knows what is on the page, from the GET request, and what it needs to do with it. The client only needs to tell the remote resource store what it did. In this pattern the controller is relegated to nothing more than acting on model data and letting the client know if it succeeded or failed. So in short.

The AJAX head design pattern forces the view and controller to work in isolation with the most minimal coupling possible. Kind of like a web service.

Tools and Code Needed

  • A application that does just about everything with AJAX.
  • Fluency with ActionController::Base#head method and HTTP status codes.
  • Site-wide exception rescues for ActiveRecord::RecordInvalid.
  • Global client-side AJAX request and response handlers.
  • Global client-side error handlers.

This may seem like a daunting list, but its pretty simple when you break it down. Let's do each one at at time with some of the current code from HomeMarks. The HomeMarks app is my first check on the list. When I did this app just about EVERYTHING was done via an AJAX call to update the page. Now on to the others.

2) Head And Status Codes

The ActionController::Base#head method and HTTP status codes are the second item on the list. The head method is very useful when you just want to respond with HTTP headers and no body content. This is typical behavior when an app responds to external web services requests. This is important to note, I chose to rely on head responses as a way to meet my design decision of not using any RJS responses from the controller.

The basic usage of the head method is simple, you just pass a symbol to the head method that is the lowercase underscore equivalent to an HTTP status code. For instance, if you wanted to respond with success, choose a status code from the 200 range, the easiest being 'OK', which would look like head(:ok). If you wanted to respond to bad authentication, you might use the 401 code like so, head(:unauthorized). Here is an example from HomeMarks restful user signup action.

1
2
3
4
5
6
7
8
9

class UsersController < ApplicationController
  # ...
  def create
    User.create!(params[:user])
    head :ok
  end
  # ...
end

There is actually a number of things going on in those 2 lines of code above. If the user is created, the AJAX request will get a simple 'OK' message via the head method. Obviously the client will need to know what to do on its own. But if errors happen what to do? This brings us to...

3) Invalid Object Handling

When Rails 2 came out, it gave us a very useful tool for our controllers, the ActionController#rescue_from method. It allows you to delegate exception handling to a method either per controller or site wide for a specific exception class. So enter the RenderInvalidRecord module, show below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

module RenderInvalidRecord
  
  def self.included(base)
    base.rescue_from(ActiveRecord::RecordInvalid, :with => :render_invalid_record)
  end
  
  protected
  
  def render_invalid_record(exception)
    record = exception.record
    respond_to do |format|
      format.html { render :action => (record.new_record? ? 'new' : 'edit') }
      format.js   { render :json => record.errors.full_messages, :status => :unprocessable_entity, :content_type => 'application/json' }
      format.xml  { render :xml => record.errors.full_messages,  :status => :unprocessable_entity }
      format.json { render :json => record.errors.full_messages, :status => :unprocessable_entity }
    end
  end
  
end

Once included in your application.rb file, every ActiveRecord::RecordInvalid exception will be handled by the render_invalid_record method. I use this pattern often and it is the primary reason I always use the bang methods that end with an exclamation point, such as create! or save!. This module above was inspired by Joshua Peek's own module of the same name but has been edited here to fit my needs. Both the "xml" and "html" response formats are not used by HomeMarks but are included here to show how this module can work for any request type.

Now, how this fits into the AJAX Head pattern. The design decision of not having RJS responses means one of two things needs to come from the response. First, if the response is a success, meaning in the 200 range, then it is assumed that the client only needs to know that all is 'OK'. Second, if the request has errors, then the response needs to communicate them. I choose JSON for the format of these errors since all AJAX request objects from Prototype automatically include a application/json accept header. So that is why I changed both the "js" and the "json" response type in the RenderInvalidRecord module to both serialize the objects full error messages into a JSON array with an HTTP status code of 422 "Unprocessable Entity" which seemed to make the most sense to me. This pretty much wraps up the server side part of the pattern. Now onto the client side. Everything from here on is personal taste on how your client will react to an "good" or "bad" response object.

4) Client-Side AJAX Handlers

I can not stress enough how much you have to bake your own functions to handle the client side reaction to both good and bad AJAX responses. The client's ability to know what to do with errors will be tightly coupled to AJAX handlers and the design choices of your site. In the examples below I have broken up methods in the same JavaScript classes to show you the parts for a specific behavior in a more concise manner. Also please note that I am using Prototype module and class organization heavily in HomeMarks. This allows me to build a Base JavaScript module and classes that mimic Rails models which inherit/mixin Base methods. I also use the Scriptaculous Builder class for creating DOM fragments too. Now back to business.

The first part of the AJAX head pattern was to use unobtrusive JavaScript. Because of this design choice, there are no remote_form_for methods in the HomeMarks view code. All forms are simple form_for methods. Below is a sample of the HomeMarksSite JavaScript class that creates AJAX handlers for all site forms. The below example is slimmed down to only show the initialization of this behavior for the signup form. But no matter which form is given this behavior the process is the same.

  • The form's submit event is handled by startAjaxForm(). It does the following:
    • Stops the normal submit behavior.
    • Adds a loading spinner in the #form_loading span.
    • Creates a new AJAX.Request using the forms own action. It also binds a function to delegate the logic of completing the form when the request is finished. More on that later in #2 below.
    • Finally it disables the form.
  • Once a response comes back from the server, the delegateCompleteAjaxForm() function will be called with both the form object and the AJAX request object. The logic of this function includes:
    • Figuring out the "mood" of the request using getRequestMood() function which is part of my base module.
    • Sends the form and mood to the completeAjaxForm() function. This function will replace the loading spinner with an image that reflects a good or bad response.
    • If the mood is good, meaning the response status code was within a 200 range, then the function will delegate to a function specific for that forms success using a simple case statement.
    • If this mood is bad, meaning the response status code not within a 200 range, the form will be enabled and the response JSON errors will be displayed. Currently I am using a modal class for this. Again more on that later. You may also note that my base module has a function called messagesToList() that process the response into a DOM <ul> list.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

var HomeMarksBase = {
  getRequestMood: function(request) {
    return (request.status >= 200 && request.status < 300) ? 'good' : 'bad';
  },
  messagesToList: function(request) {
    var messages = request.responseJSON;
    var messageList = UL();
    messages.each(function(message){ 
      messageList.appendChild( LI(message.escapeHTML()) );
    });
    return messageList;
  }
};
var HomeMarksSite = Class.create(HomeMarksBase,{ 
  initialize: function() {
    this.signupForm = $('signup_form');
    this.initEvents();
  },
  startAjaxForm: function(event) {
    event.stop();
    var form = event.findElement('form');
    var options = Object.extend({imgSrc:'loading_invert.gif'}, arguments[1] || {});
    var loadArea = form.down('#form_loading');
    var imgTag = IMG({src:('/images/'+options.imgSrc)});
    $(loadArea).update(imgTag);
    new Ajax.Request(form.action,{
      onComplete: function(request){ this.delegateCompleteAjaxForm(form,request) }.bind(this),
      parameters: form.serialize(true)
    });
    form.disable();
  },
  delegateCompleteAjaxForm: function(form,request) {
    var mood = this.getRequestMood(request);
    this.completeAjaxForm(form,{mood:mood});
    if (mood == 'good') { 
      switch (form) { 
        case this.supportForm : this.completeSupportForm(request); break;
      }
    }
    else { 
      form.enable();
      if (!request.responseText.blank()) {
        var flashHTML = DIV([H2('Errors On Form!'),this.messagesToList(request)]);
        this.flashModal('bad',flashHTML);
      };
    };
  },
  completeAjaxForm: function(form) {
    var options = Object.extend({mood:'good'}, arguments[1] || {});
    var loadArea = form.down('#form_loading');
    var completeId = 'complete_ajax_form_' + form.id;
    var imgSrc = '/images/'+options.mood+'.png';
    var moodHtml = SPAN({id:completeId,className:'m0 p0'}, [IMG({src:imgSrc})]);
    $(loadArea).update(moodHtml);
    setTimeout(function() { $(completeId).fade(); },3000);
  },
  completeSignupForm: function(request) {
    var message = "Thank your for signing up for your own HomeMarks page. An email has been sent to " +
      "your address along with a link to activate your account. If you have not done so already, please " +
      "take a moment to read the HomeMarks documentation.";
    var flashHTML = DIV([H2('Signup Complete:'),P(message)]);
    this.flash('good',flashHTML);
  },
  initEvents: function() {
    if (this.signupForm) { this.signupForm.observe('submit', this.startAjaxForm.bindAsEventListener(this)); };
  }
});

5) Client-Side Error Handling

Currently I have 3 client-side options that I can use to present errors back to the user from the JSON array in AJAX response. The first I started out with was using the alert() function. That got boring real quick, but I left my implementation below which is part of the base class, called messagesToAlert().

The second slightly more interesting way was to create a flash message that used the same XHTML/CSS already present in my layout file. This was real easy to implement. Basically using the ERB fragment below in my layout, I guaranteed that I had 3 unique DOM elements for each of my flash message styles, good/bad/indif. Now all I had to do was to create an accessor in my HomeMarksSite class. I choose this.flashes. I then created a clearFlashes() function and then a flash() function that would take the mood and the HTML to embed, viola – I can now call this.flash.('good',someHTML) and I get the same type of flash one would have seen if I set it in the controller and rendered. Note, this is what I used on the the signup form success above.

1
2
3
4
5
6

<% [:good,:bad,:indif].each do |key| %>
  <div id="flash_<%= key %>" class="flash_message" style="display:<%= flash[key].blank? ? 'none' : 'block' %>;">
    <span><%= flash[key] %></span>
  </div>
<% end %>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

var HomeMarksBase = {
  messagesToAlert: function(request) {
    var messages = request.responseJSON;
    var alertText = messages.join(".\n");
    if (alertText.endsWith('.')) { alert(alertText); } else { alert(alertText+'.'); };
  },
  messagesToList: function(request) {
    var messages = request.responseJSON;
    var messageList = UL();
    messages.each(function(message){ 
      messageList.appendChild( LI(message.escapeHTML()) );
    });
    return messageList;
  }
};
var HomeMarksSite = Class.create(HomeMarksBase,{ 
  initialize: function() {
    this.flashes = $$('div.flash_message');
  },
  clearFlashes: function() {
    this.flashes.invoke('hide');
    this.flashes.invoke('update','');
  },
  flash: function(mood,html) {
    this.clearFlashes();
    var moodFlash = this.flashes.find(function(e){ if (e.id == 'flash_'+mood) {return true}; });
    moodFlash.update(html);
    moodFlash.show();
    $('site_wrapper').scrollTo();
  }
});

Lastly, and probably the coolest, is the HomeMarksModal JavaScript class I created. This is my new default way of errors to a user. It can be seen in all my video examples on this page. To the right is another example of the login form that uses the same handlers described above. The HomeMarksModal JavaScript class is a bit large to paste inline in this article, but if you want to see the latest version, you can always get it from the master branch of the homemarks_core project on Github.com. When you bind an instance of this object to the window/document, it will automatically crate the modal HTML using Builder. If you want to use it, make sure to get the CSS and images the project too. Below is a function that I put in my site class that allows me to call this.flashModal('good',someHTML)).

1
2
3
4
5
6
7

var HomeMarksSite = Class.create(HomeMarksBase,{ 
  flashModal: function(mood,html) {
    var moodColor = this.flashMoodColors.get(mood);
    HmModal.show(html,{color:moodColor});
  }
});

Wrapping It Up

This article turned out a lot longer than I had wanted. The AJAX head pattern is pretty simple and it is really fun to see a two line user signup action yield such interactive results. Something also feels good about not putting view code in the controller, yes you can look at inline RJS as view code. Sure it requires that you do a lot more JS work, but there are benefits.

Image you have team of programmers, one person can stay in model/controller land while the other stays on the view/javascript. The model/controller person would write their own tests (PLEASE TAKE A LOOK AT MINE), while the view/javascript person might even take the initiative to use selenium to test the JavaScript. At the time of this writing, I have not done any selenium tests since the plugin for rails is not compatible for 2.0. That last benefit I could think of for this design pattern is that you could easily update view code without restarting your rails app. Just update the JavaScripts and your good to go.

In closing, please let me know what you think. Perhaps you have a better name for the design pattern? Perhaps someone else has a name for it already?

Resources

RD

  HOMEPAGE  | May 25th, 2008 at 01:47 AM
RD what a great effort.. I liked your spirit to try and go for complete unobtrusive even with forms even when rails provide easy remote_form_for. Not sure if I will ever go for complete unobtrusiveness but a great lessons to learn reading this writeup

Ken Collins

  HOMEPAGE  | May 25th, 2008 at 08:56 AM
Ken Collins

Thank you very much for the comment. So far the pattern is proving to be a big time saver. Although it really does relay on the fact that you can pick up the controller slack in your own JS classes. For, instance, I'm working on the ColumnsController#new action. All lines commented out will go away and be client side JS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

def new
  current_user.columns.create!
  head :ok
  # @remove_welcome = true if (@user.columns.length == 1)
  # render :update do |page|
  #   page.remove :welcome_box if @remove_welcome
  #   page.insert_html :top, 'col_wrapper', {:partial => 'new_col', :locals => {:col => @column}}
  #   page.visual_effect :pulsate, "col_#{@column.id}"
  #   page.reorder_then_create_box_sortables(@column,@user)
  #   page.create_column_sortable
  #   page.make_msg('good','Column created.')
  # end
end

Mike Nicholaides

  HOMEPAGE  | May 25th, 2008 at 05:57 PM
Mike Nicholaides Hey! I've actually been thinking about this design pattern for a couple weeks, and I'd really like to hear more in the future how it works out, especially in terms of maintainability and testability. Great article!

CLR

  HOMEPAGE  | May 27th, 2008 at 10:57 AM
CLR That is not a design pattern. Essentially you are moving the controller to the client, which is a Remote Evaluation architecture style. What you describe is a fine idea, but it is an implementation, not a pattern. Thanks! -CLR

Ken Collins

  HOMEPAGE  | May 27th, 2008 at 11:29 AM
Ken Collins @CLR Agreed :)

Ericson Smith

  HOMEPAGE  | May 28th, 2008 at 06:44 PM
Ericson Smith Its a good idea for certain kinds of sites (intranets, highly dynamic content, etc). However, its impractical for public-facing sites, since people would never be able to deeply find you in the search engines. I searched this page for the word "degrade" (and variants), and nothing came up. No degradation at all. Good idea in some minimal instances where content is not required to be indexed, bad idea for most public use-cases.

Ken Collins

  HOMEPAGE  | May 28th, 2008 at 11:21 PM
Ken Collins @Ericson Smith - Yea I totally agree, but I think that is a moot point. First, remember that I am talking about the HomeMarks site and the latest version, not yet deployed. Second, consider that every page on the public facing site, even those with AJAX forms that follow this implementation are behind a GET request. All the page updates, like form errors, user input would never be seen by a bot anyway. Remember bots do not do forms and they REALLY do not do anything AJAX or non GET, like POST/PUT/DELETE in the restful sense.

Dirk Breuer

  HOMEPAGE  | May 29th, 2008 at 01:00 PM
Dirk Breuer This truly sounds nice and I definitely need more experience with unobtrusive JavaScript. I like this idea very much and your approach looks promising. But I have two issues:

1. You want to abandon inline RJS which I can fully understand. It is ugly and spoils your controller code. But you can use dedicated RJS templates. Which will need still a lot of work to be totally unobtrusive I think. But you still have the power to use dynamic creation of JavaScript code, depending on the context.
2. The second issue I have is somewhat related to the first. Due to the fact that you relocate all your interaction logic including the required content to the client side you lose any chance to have localization or internationalization. Which is crucial for bigger sides. But I think one could address this issue with the first one.

Anyway, this was a great article which I had fun to read.
Keep up the good work.

Ken Collins

  HOMEPAGE  | May 29th, 2008 at 11:05 PM
Ken Collins

@Dirk About #1, I really thought hard about trying to keep RJS, even in separate files, but it is really really hard to stay object oriented when using RJS. It really is a very procedural way of issuing JS commands. About the best OO way of using RJS is to drop down to the page<< command and just dump raw JavaScript commands that you know their exists an object for. Something like:

page.<< "HmSite.flash('good','Message');"

But once you start doing that you might as well totally decouple and just stay full OO in the view land

About the localization, yea, I hear ya. The good thing is that this might not be that hard. Consider that all object errors still come from the app via JSON, there could be some helper that works in tandem with the rescue_from controller action so the array of error messages gets localized. So that solves the problem for errors and of course all GET requests can render localized content like a rails app normal would. That just leaves the simple flash messages generated by JavaScript, which are few. I'm sure there would be an easy JS way of telling which language the browser wants and then changing that string?

Bryan Ray

  HOMEPAGE  | June 24th, 2008 at 11:25 AM
Bryan Ray Hey Ken ... first off: great article. There's a lot of good ideas and great example code. Second: I've often struggled with this implementation, due to a few concerns: 1. I'm not a huge fan of Javascript and I admit this is probably just my lack of knowledge in that arena, but debugging javascript has always been a major pain in the ass. Yes, Firebug helps relieve that pain, but I still cringe when I have to go in and much around with things. 2. This approach is *so* javascript intensive. Look at all the code you've had to write for a simple form submission. 3. The unobtrusive approach, in my opinion, tends to lead to unnecessariliy 'hidden' (unobtrusive, obviously) functionality. Which is fine, I think, as long as a great deal of caution is taken to point future developers in the right direction. For instance ... when a new developer comes on and looks at the views and sees a simple "form_for" and then goes to look at the rendered result and finds that the form is actually not a simple form submission ... she is now forced to go dig through javascript to locate how the unobtrusive javascript is being attached. This functionality could be 'hidden' just about anywhere. Anyways ... don't mean to degrade your article by any means. It's a great read and I will definitely give some of your implementations a try. Best Regards.

Bryan Ray

  HOMEPAGE  | June 24th, 2008 at 11:28 AM
Bryan Ray

Hey Ken ... first off: great article. There's a lot of good ideas and great example code.

Second: I've often struggled with this implementation, due to a few concerns:

I'm not a huge fan of Javascript and I admit this is probably just my lack of knowledge in that arena, but debugging javascript has always been a major pain in the ass. Yes, Firebug helps relieve that pain, but I still cringe when I have to go in and much around with things.

This approach is *so* javascript intensive. Look at all the code you've had to write for a simple form submission.

The unobtrusive approach, in my opinion, tends to lead to unnecessariliy 'hidden' (unobtrusive, obviously) functionality. Which is fine, I think, as long as a great deal of caution is taken to point future developers in the right direction.

For instance ... when a new developer comes on and looks at the views and sees a simple "form_for" and then goes to look at the rendered result and finds that the form is actually not a simple form submission ... she is now forced to go dig through javascript to locate how the unobtrusive javascript is being attached. This functionality could be 'hidden' just about anywhere.

Anyways ... don't mean to degrade your article by any means. It's a great read and I will definitely give some of your implementations a try.

Best Regards.