How to create toggle buttons using Ruby on Rails, ajax, and unobtrusive javascript. Straight-forward and boring, just how I like it.
Built with jQuery and CoffeeScript, this page reflects the web development practices of its time rather than the Hotwire-based approach I use today.
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.
This webpage is not hosted by Rails, so the above UX is just a simulation.
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.#update and #destroy actions for toggling on and off respectively.# 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(
"star",
"Favorite",
url,
method: "DELETE",
remote: true,
class: "favorite btn btn-primary",
)
else
link_to_with_icon(
"star",
"Favorite",
url,
method: "PUT",
remote: true,
class: "favorite btn",
)
end
end
def link_to_with_icon(icon, title, url, options = {})
idle = content_tag(:span, lucide_icon(icon, class: "icon"), data: { icon: "idle" })
spinner = content_tag(:span, lucide_icon("loader-circle", class: "icon icon-spin"), class: "hidden", data: { icon: "loading" })
title_with_icon = idle << spinner << " ".html_safe << h(title)
link_to(title_with_icon, url, options)
end
end
# Gemfile
gem "jquery-rails"
gem "lucide-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() {
$(this).find('[data-icon=idle]').addClass('hidden');
$(this).find('[data-icon=loading]').removeClass('hidden');
});
// Change the link's icon back after it's finished.
$(document).on('ajax:complete', function(e) {
$(e.target).find('[data-icon=loading]').addClass('hidden');
$(e.target).find('[data-icon=idle]').removeClass('hidden');
});
// Don't fail silently
$(document).ajaxError(function(event, jqxhr) {
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.