Ajax, Hobo Style
Posted by
Tom | December 5, 2006 18 Responses comments

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? :-)

Reader Comments Add your comment »

Interesting. The last dryml makes it hard to distinguish HTML from proprietary code though.
Also, be careful when wrapping block level elements. To remain valid, that span must turn to div :-)

The fact that defined tags look the same as HTML primitives is deliberate — think of a GUI library where you have classes that provide user-interface widgets like buttons, menus etc. If you create your own custom widget, you want it to blend right in with the existing ones.

Then again, it would be pretty easy to customise the HTML mode of your editor to use a different colour for HTML tags.

Good point about the <span> tag. Unfortunately the part mechanism doesn’t know what you’re going to put inside the part, so it doesn’t know whether to use a span or a div.

There’s a change in the part mechanism on the way that might also offer a fix to this problem.

Minor nit, but the line
<% formremotetag :url => “/tasks”, :method => :post do %>

should read

<% form_remote_tag :url => "/tasks/create", :method => :post do %>

haakon – this is the way things work with RESTful routes. The fact that it’s an HTTP POST means the create action will be invoked at the /tasks url. A GET to the same url would invoke the index action.

The is very nice indeed,but as Srđan pointed out, the custom tags are hard to distinguish from the HTML. Why not use a qualified name? like <dry:form>. That would also make the tags more declarative.

As I said above, I want the custom tags to look like HTML. In my Hobo apps, my views contain 80% — 90% Hobo tags, having dry: or even just d: in front of them all would be pretty grim.

I really don’t see a problem. In a Ruby class, how do you distinguish between that class’s own methods and inherited methods?

Once the module system is working, you can if you want keep all your tags in a module, and then they look like

<m.tag ... />

It deliberately uses a dot rather than a colon, as discussed in this thread.

I’m not quite sure what you mean by “more declarative”. Because you can re-use the “simple” names like form and table?

Ah, forgive my ignorance about the new restful stuff. I hadn’t restarted the server after putting this line in the right spot in the routes.rb:

map.resources :todo_lists, :tasks

I also found that “include Hobo::AjaxController” failed with an “Uninitialized Constant”.

My slow understanding aside, this all looks extremely cool. I like how easy it is to define the tags, and getting an app with users / authentication baked in makes tons of sense. You are taking a good page from the Django book.

haakon – you’ve hit a problem with this post getting out of date with respect to the code. Try

hobo_controller

Instead of

include Hobo::AjaxController

You hit a sore point.
Extrefox

Is there a reason i am getting invalid tag on hoborapidjavascripts?

Tom: I wholeheartedly agree with you about making the dryml look like HTML. It might make sense to use <namespace:tagname> if lots of us share tags and we want to avoid name collisions, but this is not going to be a problem between the base DRYML tags and HTML. And I would assume that any developer worth their salt knows all the HTML tags and won’t have any trouble at all know which ones are therefore DRYML.

I suppose if enough people wanted it, it probably wouldn’t be hard for you to add the ability to use a verbose syntax, such as <dry:page>, but I for one would never use it.

Can I you jQuery with Hobo?
I’m using jRails so basic rails ajax helpers are works well.

But If Hobo is using prototype and script.aculo.us internally, I can’t(?) use jQuery(or must use jQuery with prototype together).

@Chihyung: actually jQuery is designed to make it easy to use alongside prototype and scriptaculous. There’s info on their site about how to do this.

Is there a way to use hobo and jQuery and remove all references to all prototype/script.aculo.us stuff? Thanks in advance.

Victor — not really, not an easy way. But we’re thinking of moving to jQuery anyway.

Tom – thanks for responding. How soon will you move? It seems all js stuff are in one file hobo_rapid.js? It will be good for a lot of people since jQuery are getting so popular.

Victor — I really have no idea when this might happen. There are so many other priorities that it might not happen for a good while, or even at all. Why don’t you have a go at moving hobo-rapid.js in that direction yourself?

Tom – I don’t understand most of the stuff in hobo-rapid.js. If I can , I will let you know.


Write a Comment

Comments are formatted using markdown. To include code, either quote it in `backticks` or indent a code-block by 4 spaces.