Published by Dan on Apr 16, 2014

Ajax Toggle Buttons

How to create toggle buttons using Ruby on Rails, ajax, and unobtrusive javascript. Straight-forward and boring, just how I like it.

Filed under Features

Introduction

Toggle buttons are useful for communicating and changing state. Here's what the user sees:

This pattern presents itself in every one of my applications, so I wanted to document how I implement it in Ruby on Rails, as it is a good introduction to ajax, unobtrusive javascript, and Rails handling javascript requests.

Demo

Click the buttons below and look at your web-developer tools for how the ajax response swaps the links, but note this site runs on a static webserver, so the paths and HTTP methods don't mirror an actual Ruby on Rails application.

  Favorite   Locked

How it Works

  1. A Rails helper method renders the button in its current state.
  2. This button has an href along with data-method, data-type=script, and data-remote=true attributes which instruct jquery_ujs.js to perform an ajax request and evaluate the result as javascript.
  3. When the user clicks either button, a jQuery listener switches to the loading icon while another listener switches it back on completion.
  4. In Rails, the main resource has a child which provides #update and #destroy actions for toggling on and off respectively.
  5. After saving the change, Rails responds with one line of javascript that replaces the button with its new state.

The Code

# models/post.rb
class Post < ActiveRecord::Base

  def favorited?
    favorited_at != nil
  end

  def favorite
    self.favorited_at = Time.now
  end

  def favorite!
    favorite
    save!
  end

  def unfavorite
    self.favorited_at = nil
  end

  def unfavorite!
    unfavorite
    save!
  end

end

# config/routes.rb
resources :posts do
  resource :favorite, only: %w(update destroy)
end

# controllers/posts/favorites_controller.rb
class Posts::FavoritesController < ApplicationController

  before_action :load_post

  def update
    @post.favorite!
  end

  def destroy
    @post.unfavorite!
  end

  private

  def load_post
    @post = Post.find(params[:post_id])
  end

end

# helpers/posts_helper.rb
module PostsHelper

  def link_to_toggle_post_favorite(post)
    url = post_favorite_path(post)

    if post.favorited?
      link_to_with_icon('icon-star', 'Favorite', url, {
        method: 'DELETE',
        remote: true,
        class: 'favorite btn btn-primary',
      })
    else
      link_to_with_icon('icon-star', 'Favorite', url, {
        method: 'PUT',
        remote: true,
        class: 'favorite btn',
      })
    end
  end

  def link_to_with_icon(icon_css, title, url, options = {})
    icon = content_tag(:i, nil, class: icon_css)
    title_with_icon = icon << ' '.html_safe << h(title)
    link_to(title_with_icon, url, options)
  end

end

# Gemfile
gem 'jquery-rails'

// assets/javascript/application.js
//
// jquery_ujs allows us to use 'data-remote',
// 'data-type', and 'data-method' attributes
//
//= require jquery
//= require jquery_ujs
//= require_tree .

/* assets/javascripts/loading.js */

// This isn't necessarily specific to toggle buttons
$(function() {

  // Change the link's icon while the request is performing
  $(document).on('click', 'a[data-remote]', function(event, b, c) {
    var icon = $(this).find('i');
    icon.data('old-class', icon.attr('class'));
    icon.attr('class', 'icon-refresh');
  });

  // Change the link's icon back after it's finished.
  $(document).on('ajax:complete', function(e) {
    var icon = $(e.target).find('i');
    if (icon.data('old-class')) {
      icon.attr('class', icon.data('old-class'));
      icon.data('old-class', null);
    }
  })

  // Don't fail silently
  $(document).ajaxError(function( event, jqxhr, settings, exception ) {
    if (jqxhr.status) {
      alert("We're sorry, but something went wrong (" + jqxhr.status + ')');
    }
  });

})

<%# views/posts/show.html.erb %> 
<%= div_for @post do %> 
  <%= link_to_toggle_post_favorite @post %> 
<% end %> 

/* views/posts/favorites/update.js.erb */
$('#post-<%= @post.id %> .favorite').replaceWith("<%=j link_to_toggle_post_favorite @post %>");

/* views/posts/favorites/destroy.js.erb */
$('#post-<%= @post.id %> .favorite').replaceWith("<%=j link_to_toggle_post_favorite @post %>");

Wrap-up

Unobtrusive javascript allows the application logic to stay in Ruby.

We follow "The Rails' Way" to #update and #destroy in the controller, which will help the application gracefully grow when we add more functionality to posts like additional toggles, fields, or a public RESTful API.

Let me know what else you think could be improved.

Dan

© 2012-2019 Dan Cunning. All rights reserved.