Tuesday, September 06, 2011

Simple Ajax property toggle in Rails 3.0

Entry-level overview of one way to add trivial Ajax functionality to a Rails 3.0 app, originally written for a specific audience. The repository is on github.

Let's say we have an Article model with an "approved" flag. We need to be able to toggle this flag. Normal scaffolding would have us view the article, click a checkbox, and submit. We'll keep that functionality, but add a simple Ajax-based toggle on the articles index page.

Model Generation
rails g scaffold Article name:string approved:boolean

Original Model Index View

The default scaffolding includes the following chunk, repeated for each article.
 <td><%= article.name %></td>
 <td><%= article.approved %></td>

An Approving Helper
"True" and "false" are a yucky user experience, what we really want is "approved" and "un-approved". We'll think ahead, and turn it instead into text suitable for a command link. We could also use images, but we'll stick with text for now.
def approve_link_text(approvable)
  approvable.approved? ? 'Un-approve' : 'Approve'

Two quick things about how this was written.

1) It's not tied to articles; it'll quack at anything with an "approved?" method.
2) The parameter is named something that provides a hint to future devs, more useful in an IDE that provides popup help.

Add a Method to the ArticlesController to Toggle Approval Status
Two-step process: first, add a method to the articles resources routing.
resources :articles do
  get 'toggle_approve', :on => :member

Then the method itself (this is not the final version).
def toggle_approve
  @a = Article.find(params[:id])
  render :nothing => true

Update the Template
Our first step will just toggle the attribute without any feedback. It's actually a one-liner, just make the true/false "approved" text from before be a link to our new action.
<%= link_to approve_link_text(article), 
            :remote => true %>

The :remote => true turns it into an Ajax request. Not too shabby.

If we refresh the page after clicking the link we'll see that the text has changed. Approved articles will have an "Un-approve" link, un-approved articles an "Approve" link. Clicky-clicky, it does what we expect.

But refreshing is deeply unsatisfying.

Dynamic Feedback, Part 1

Remember when fadey-yellow things were cool? Yeah, we're all about that.

In order to change stuff on our page, we have to be able to access the appropriate DOM elements. Let's say we'd like to highlight the entire row when we toggle an article's approval status, and change the link text.

We'll use "article_n" for the article rows, and "approval_link_n" for the links.

<% @articles.each do |article| %>
  <%= article.name %>
    <%= link_to approve_link_text(@article), toggle_approve_article_path(@article), :remote => true, :id => "approve_link_#{@article.id}" %>

Dynamic Feedback, Part 2

Now we can access the DOM elements we care about on a per-article basis. How do we make them change? By creating a JavaScript template for our action, just like we usually create HTML templates. Rails will execute the returned JavaScript. The JavaScript template can contain ERb constructs, also just like HTML templates.

Note that some people don't like creating JavaScript this way. An alternative is to create DOM elements that our JavaScript can pull from. That's fine, but honestly, for simple things like this, I don't have a big problem with doing it the "old-fashioned way".

Our action is called "toggle_approve". We name JavaScript templates the same way we do ERb templates, so we'll create a toggle_approve.js.erb.
$("#approve_link_<%= @article.id %>").text("<%= approve_link_text(@article) %>");
$("#article_<%= @article.id %>").effect("highlight");

Line 1 sets the link text to Approve/Un-approve using the same helper we used in the HTML template.
Line 2 highlights the row we just updated.

That's it--we no longer have to refresh our page--the text of our link changes after the property is toggled, the row is highlighted, and we're all set!


Ray said...

Love it. So simple. Thanks. However the JQuery references a slightly different ID for the link. 'approve' vs 'approval'.

Ray said...

I'm almost there with this. My toggle link will toggle the record in the database, however the text returned is the opposite of that expected - almost as if the approved? statement is tested before the record changes in the database. Did you encounter this problem?

Dave Newton said...

@Ray: Nope. The "toggle!" function updates in the database immediately, so will be reflected in the rendered JavaScript sent back.

Are you sure you're reading it right? The link text is the command: if it's approved, the command is to "un-approve", it's not "un-approved".

Josh said...

Awesome tutorial. Just what I was looking for!