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

Clicks on Favorite

An ajax request is started and the button becomes Favorite

When the request succeeds the button becomes Favorite

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.

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

1. A Rails helper method renders the button in its current state.1. 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.

1. When the user clicks either button, a jQuery listener switches to the loading icon while another listener switches it back on completion.

1. In Rails, the main resource has a child which provides #update and #destroy actions for toggling on and off respectively.

1. After saving the change, Rails responds with one line of javascript that replaces the button with its new state.

# 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 %> ");

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.