Pragmatic Rails: Let’s do AJAX-backed Sidebar Widgets right, Jim!

{{{
Days ago “I explained how to write a sidebar widget in Rails”:http://nicksda.apotomo.de/2010/11/lets-write-a-reusable-sidebar-component-in-rails-3/, using the view component framework “Cells”:http://github.com/apotonick/cells. This was fun.

The sidebar widget.

Clicking a tag leads to a page reload, where the _Recent posts_ list *displays items matching the tag*, only. It worked great, however, each click needs a real HTTP request and a reload – *let’s use AJAX to handle that*.

h3. Rails and AJAX? Apotomo!

Widgets+Rails usually means you’re better off using “Apotomo”:http://apotomo.de , a new kid on the block. “Apotomo”:http://github.com/apotonick/apotomo is a web component framework for Rails, I will officially announce it in a separate post soon.

Let’s discuss how I *converted the _Recent posts_ cell into an interactive widget*. If that’s too brief, there is a detailed “in-depth tutorial”:http://apotomo.de/peters-guide.html here, just follow the links at _Learn_.

Check out “the repository of this example app”:https://github.com/apotonick/cells-blog-example/tree/with-apotomo *if you wanna play around*.

h3. Getting a cell interactive

We need the Apotomo gem, so I put it in the @Gemfile@.

gem 'rails', '3.0.1'
gem 'cells'
gem 'apotomo', "~>1.0"
# ... and so on

I would never forget to run @bundle install@, Freddi!

In order to make a cell a widget, I inherit from @Apotomo::Widget@.

class PostsWidget < Apotomo::Widget
  def display
    @tag    = param(:tag)
    @posts  = @tag ? Post.tagged_with(@tag) : Post.recent
    render
  end
  
  def tag_cloud
    @tags = Post.tag_counts_on(:tags)
    render
  end
end

Nothing really changed here, so far.

Note that deriving things from @Widget@ *doesn’t involve statefulness* or anything – it’s just a nestable cell now, being aware to Apotomo’s events. Right, Ryan?

h3. Widgets are cells

The assets layout slightly changed. Here’s how my @app/cells@ directory now looks.

|-- posts_widget
|   |-- display.html.haml
|   `-- tag_cloud.html.haml
`-- posts_widget.rb

Some names changed, but *we still got our views in a separate directory*. Widgets also reside in @app/cells@, there’s no need to push another folder into Rails.

h3. Rendering a widget

After these changes, rendering the widget turns out to be fucking simple, too.

%html
  %head
    %title "The Incredible Cells Blog"
  
  %body
    #sidebar
      = render_widget 'sidebar-posts'

I just call @#render_widget@ where I used to call @#render_cell@ in the @application.html.haml@ layout.

The call referes to the widget _id_ which is defined in the controller’s widget tree. Let’s see how that works, so we can render the component.

h3. Messing up the widget tree

Usually, *widget trees are declared on the controller class layer*.

class PostsController < ApplicationController
  include Apotomo::Rails::ControllerMethods
  
  has_widgets do |root|
    root << widget(:posts_widget, 'sidebar-posts')
  end

Just include the necessary module and *setup the tree using the @has_widgets@ method*. Notice how I refer to the widget _class_ first, then I assign the _id_.

Wow- *this already renders the _Recent posts_ box!* The @#render_widget@ helper automatically invokes the @#display@ state of the widget.

h3. Triggering events

Clicking a tag still does a real request. As I want an AJAX call here, I change the @tag_cloud.html.haml@ view a bit.

#tag-cloud
  - tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class|
    = link_to tag.name, "", :class => css_class, 

      'data-event-url' => url_for_event(:tagClicked, 
        :tag => tag.name)

:javascript
  $("#tag-cloud a").click(function(e) {
    $.ajax({url: $(this).attr("data-event-url")});
    return false;
  });

The first three lines didn’t change at all. However, two new steps involved here.

* First we *set an attribute in each clickable tag link*. The @#url_for_event@ computes some url needed to trigger the @:tagClicked@ event in Rails.
* Second a small jQuery script catches click events and *fires an AJAX request to the “_event url_”*. This is that unobstrusive Javascript everybody’s talking about.

What we need to do now is *catching the @:tagClicked@ event in the _ruby_ world* and update the item list in the browser.

Luckily, Apotomo saves us from any routing and dispatching.

h3. Catching events

As *Apotomo triggers the event in the widget tree*, the widget classes are one place to define observers.

class PostsWidget  :filter
  
  def filter
    replace :state => :display
  end

  def display
  # ... and so on

I love Apotomo’s simple event system.

Again, two new steps.

* *Calling @responds_to_event@ sets the observer.* In case of fire, it invokes the @#filter@ state method.
* The @#filter@ state itself *just calls the @#display@ state* which collects posts and renders. The @#replace@ helper method simply *wraps the rendered view in a jQuery replace* statement. Note that you may do that yourself – it’s up to you to emit any Javascript.

Now, the tag filtering works in the browser.

*That’s all we need to write in order to have an AJAXed widget!* It simply works, without any magic at all.

h3. Apotomo is stable. Use it.

The framework we used here has been around for years but evolved from a “Model-driven architecture beast” into a small, stable widget framework based on Cells.

It is meant for RIA projects, dashboards, web desktops, well, “Apotomo”:http://github.com/apotonick/apotomo is here for helping you writing great widget-based web frontends, in a monolithic Rails environment.
}}}

Advertisements

One thought on “Pragmatic Rails: Let’s do AJAX-backed Sidebar Widgets right, Jim!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s