Chronicling the trials and tribulations of developing for the modern web.



How to make an AJAX todo list checkbox

Posted by Pat Nakajima on November 07, 2007 in Development.

One of the most compelling apps when it comes to introducing users to Rails is Ta-da Lists. Almost every Rails developer has built his/her own todo list app (including me), and almost every one of these todo list apps contains the requisite AJAX checkboxes.

I don’t know about you, but when I first tried to make my checkboxes “all ajaxy,” I had a bit of trouble deciding the best way. There’s no “checkbox_to_remote” helper in Rails. After building several todo list apps, I’m convinced that the best way to create the AJAX checkbox behavior doesn’t live in Rails at all. It lives in Prototype and LowPro, my two favorite Javascript libraries.

You’ve probably heard of Prototype before. If not, you’re probably reading the wrong blog (or you’re my mom. Hi Mom.) Prototype makes the normally somewhat ugly language Javascript as beautiful as a Ruby. In other words, it takes care of a lot of the browser incompatibilities that usually plague Javascript development, as well as adds a bunch of syntactical sugar to make developing with Javascript a more enjoyable experience.

Dan Webb’s LowPro library allows you to do a few cool things with Prototype, but the one we’ll use is its Behaviors feature. Behaviors allow you to designate different behaviors for the elements on your page based on CSS selectors such as class name, ID, etc. I wrote about this in a bit more detail here.

Let’s say you’ve built your todo list app like so:

Routes

map.resources :lists do |list|
  list.resources :todos
end

Models

class List < ActiveRecord::Base
  has_many :todos, :dependent => :destroy
  has_many :incomplete_todos, :class_name => "Todo", :foreign_key => :list_id, :conditions => ['todos.complete = ?', false]
  has_many :complete_todos,   :class_name => "Todo", :foreign_key => :list_id, :conditions => ['todos.complete = ?', true]
end
class Todo < ActiveRecord::Base
  belongs_to :list
end

Pertinent Actions

class ListsController < ApplicationController
  def show
    @list  = List.find(params[:id], :include => [:incomplete_todos, :complete_todos])
    respond_to do |format|
      format.html # default
    end
  end
end
class TodosController < ApplicationController
  def update
    @list = List.find(params[:list_id])
    @todo = @list.todos.find(params[:id])
    respond_to do |format|
      if @todo.update_attributes(params[:todo])
        flash[:notice] = 'Todo was successfully updated.'
        format.html { redirect_to(@list) }
        format.js   { render :action => "updated" } # This is a .rjs template
      else
        format.html { render :action => "edit" }
      end
    end
  end
end

The lists/show view

<ul id="incomplete_list">
  <% @list.incomplete_todos.each do |todo| %>
  <li id="list_<%= todo.list_id %>_todo_<%= todo.id %>" class="todo">
    <%= checkbox :todo, :complete, :class => "checkbox" %>
    <%= todo.name %>
  </li>
  <% end %>
</ul>

Some Javascript

Event.addBehavior({
  // Defining the checkbox behavior and assigning to the appropriate elements
  'li.todo input.checkbox:click' : function(event) {
    Todo.update(this.parentNode.id);
  }
})
// A module containing the actual function to perform the AJAX request
var Todo = {
  update: function(id) {
    var element  = $(id);
    var checkbox = element.firstDescendent();
    var list_id  = id.gsub('list_','').split('_todo_')[0];
    var todo_id  = id.gsub('list_','').split('_todo_')[1];
    var url  = '/lists/' + list_id + '/todos/' + todo_id;
    var pars = $H({
      todo: { 'complete':$F(checkbox); }
    })
    new Ajax.Request(url, { parameters: pars.toQueryString(), method: 'put' })
  }
}

And that should do it. I could have made one or ten typos, since I was just typing this code off the top of my head and haven’t actually gone about testing it. If all goes to plan though, clicking a checkbox with the class name “checkbox” within an li with the class name “todo” should submit an AJAX request. The response is up to you and probably merits another post from me at some point. Not now though. Enjoy.

commentsareclosed

Sorry, but comments are closed on this blog after 30 days.