Ajax, Hobo Style

Posted by Tom on 2006-12-05

OK, next up we’re going to see what Hobo brings to the Ajax table. In a nutshell – the ability to refresh fragments of a page without pushing them out into separate partial files. To see how that works, let’s knock up a quick demo application.

We’re going to build on the todo-list demo app from an earlier post, so if you want to follow along you should start with that post. Just to recap, the app is trivially simple, consisting of a TodoList model which has_many :tasks and a Task model which belongs_to :todo_list. We created a controller for to-do lists with just a show action, and we implemented a DRYML view for that action.

We’re going to add an ajaxified “New Task” form on that same page. Let’s do the back-end first, so that it’s possible to create tasks. We’ll start by being good modern Rails citizens, and switch to RESTful routing. Add this line to routes.rb:

map.resources :todo_lists, :tasks

Now create our controller

$ ./script/generate controller tasks

app/controllers/tasks_controller.rb

class TasksController < ApplicationController

  def create
    Task.create(params[:task])
  end

end

Now we’ll add a simple ajax form to todo_lists/show.dryml. We’ll use the familiar remote_form_tag helper. We’ll use raw HTML for the form controls, just to be clear about exactly what’s going on, What we won’t do, for now, is deal with refreshing the page.

app/views/todo_lists/show.dryml

<head>
  <%= javascript_include_tag :defaults %>
</head>
<body>
  <h1>Todo List: <name/></h1>

  <ul_for attr="tasks"><name_link/></ul_for>

  <% form_remote_tag :url => "/tasks", :method => :post do %>
    <input type="hidden" name="task[todo_list_id]"
                         value="<%= this.id %>"/>
    <input type="text" name="task[name]"/>
    <input type="submit" value="New Task"/>
  <% end %>
</body>

That should work as is, although you’ll have to manually reload the page to see any new tasks. Let’s fix that, Hobo style.

With DRYML, any fragment of the page can be marked as a part. A part can be rendered separately from the rest of the page, just like a partial. To create a part, just give any element a part_id. For this demo we’ll add it to the ul_for tag.

app/views/todo_lists/show.dryml

<head>
  <%= javascript_include_tag :defaults %>
</head>
<body>
  <h1>Todo List: <name/></h1>

  <ul_for attr="tasks" part_id="task_list"><name_link/></ul_for>

  <% form_remote_tag :url => "/tasks", :method => :post do %>
    <input type="hidden" name="task[todo_list_id]"
                         value="<%= this.id %>"/>
    <input type="text" name="task[name]"/>
    <input type="submit" value="New Task"/>
  <% end %>
</body>

Having done that, reload the page in the browser and have a look at the source. You should see something like (with bits cut out for the sake of clarity):

Generated HTML Source

<head>
  <!-- A BUNCH OF JAVASCRIPT INCLUDES -->
</head>
<body>
  <h1>Todo List: Launch Hobo!</h1>

  <span id='task_list'>
  <ul>
	<!-- A BUNCH OF LIST ITEMS -->
  </ul>
  </span>

<!-- THE FORM HERE -->
</body>

<script>
var hoboParts = {}
hoboParts.task_list = 'todo_list_1'
</script>

The important bits to note are the <span> with the same ID as our part, and the JavaScript snippet at the end. The JavaScript was generated by Hobo to keep track of which model objects are displayed in which parts.

We can ask for a part to be updated, simply by adding a few parameters to the request. Hobo provides tags to make this blissfully easy, and we’ll have a look at those shortly. For now though, just to show there’s no magic going on, we’ll add them by hand using hidden fields.

The parameters we need are:

  • part_page: The path of the current page template, e.g. “todo_lists/show”. (Future development: maybe we can do away with this and use the HTTP referrer instead)

  • render[][part]: The name of the part we’d like to refresh. e.g. task_list

  • render[][object]: The “DOM ID” of the “context object” for that part. e.g. todo_list_1

Update the form as follows:

app/views/todo_lists/show.dryml (fragment)

<% form_remote_tag :url => "/tasks", :method => :post do %>
  <input type="hidden" name="task[todo_list_id]"
                       value="<%= this.id %>"/>
  <input type="text" name="task[name]"/>
  <input type="submit" value="New Task"/>

  <input type="hidden" name="part_page" value="todo_lists/show"/>
  <input type="hidden" name="render[][part]" value="task_list"/>
  <input type="hidden" name="render[][object]"
                       value="todo_list_<% this.id %>"/>
<% end %>

We now need to upgrade our controller to recognize this “render request”. That just requires including a module, and calling one method:

app/controllers/tasks_controller.rb

class TasksController < ApplicationController

  include Hobo::AjaxController

  def create
    this = Task.create(params[:task])
    hobo_ajax_response(this)
  end

end

The hobo_ajax_response method needs a page context. As you can see we’re passing in the object just created.

You should now have a working ajax “New Task” feature.

The code might work but it’s pretty ugly (heh). Let’s clean it up using the appropriate Hobo tags. The tags we need come from a tag library that’s provided with Hobo – Hobo Rapid. Hobo Rapid is a very general purpose tag library that makes it extremely quick and easy to do the bread-and-butter stuff: links, forms, ajax…

We’ll look at Hobo Rapid in more detail in another post. For now we’ll see how to pretty-up our ajax demo.

To include Hobo Rapid in your application:

$ ./script/generate hobo_rapid

Then edit your view to look as follows:

app/views/todo_lists/show.dryml

<head>
  <%= javascript_include_tag :defaults %>
  <hobo_rapid_javascripts/>
</head>
<body>
  <h1>Todo List: <name/></h1>

  <ul_for attr="tasks" part_id="task_list"><name_link/></ul_for>

  <create_form attr="tasks" update="task_list">
    <edit attr="name"/>
    <submit label="New Task"/>
  </create_form>
</body>

Yep - that’s it :-) Try it – it should be working.

The <create_form> tag can be read as: Include a form which will first create an object in the collection “tasks” of the current context (the TodoList), and then update the “task_list” part. Note that you don’t need to say anything about how to update that part. Hobo knows.

<booming-voice>AND NOW [drum-roll] THE GRAND FINALE…</booming-voice>

How easy is it to update multiple parts in one go? For example, suppose the page had a count of the number of tasks – that would need updating too. We’ll use another handy little tag from Hobo Rapid: <count> (Note this tag doesn’t have any special ajax support. We could have used a regular ERB scriptlet and the ajax would still work). And for bonus marks, we’ll DRY up all those attr='tasks' using a <with> tag, which just changes the context.

app/views/todo_lists/show.dryml

<head>
  <%= javascript_include_tag :defaults %>
  <hobo_rapid_javascripts/>
</head>
<body>
  <h1>Todo List: <name/></h1>

  <with attr="tasks">
    <ul_for part_id="task_list"><name_link/></ul_for>
    <p><count part_id="task_count"/></p>

    <create_form update="task_list, task_count">
      <edit attr="name"/>
      <submit label="New Task"/>
    </create_form>
  </with>
</body>

To update multiple parts, just list the part names in the update attribute.

What more could you ask for? :-)



(edit)