Published by Dan Cunning 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:

  • Clicks on
  • An ajax request is started and the button becomes
  • When the request succeeds the button becomes

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

  Favorite   Locked

This webpage is not hosted by Rails, so the above UX is just a simulation.

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 < ApplicationRecord
  def favorited?
    favorited_at != nil
  end

  def favorite
    self.favorited_at = Time.zone.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.