Ruby on Rails archive

Loading Buttons

Converting action buttons into loading buttons is a common way for AJAX applications indicate that it’s waiting on the server to verify an action and respond.

Built with jQuery and CoffeeScript, this page reflects the web development practices of its time rather than the Hotwire-based approach I use today.

Introduction

Loading buttons are a common user-experience concept used to indicate the state between starting and completing an action. They are most useful when server-side logic decides what happens next, such as displaying validation errors or a success message.

Single page applications may bypass loading buttons to appear more responsive since they use client-side logic, but loading buttons still play a large role in the standard MVC Rails application.

represents an action for the user to perform.

indicates the action in processing via a loading icon and disabled button.

Demo

Click the buttons below for a live demonstration.

This webpage is not hosted by Rails, so the above UX is just a simulation.
A "Log out" button would normally redirect you to the sign-in page.

How it works

  • A Rails helper method renders a button with data-remote=true and data-loading=true attributes.
  • data-remote=true instructs jquery_ujs.js to begin an ajax request.
  • data-loading=true instructs a jQuery listener to disable the button and insert a spinner.
  • Your Rails application determines what happens after the request finishes:
    • Reset the button's state
    • Redirect to another page

The Code

# config/routes.rb
resources :sessions, only: %w[new destroy]
# controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def destroy
    reset_session
  end
end
# helpers/sessions_helper.rb
module SessionsHelper
  def link_to_log_out
    link_to_with_icon(
      "log-out",
      "Log out",
      url,
      method: "DELETE",
      remote: true,
      class: "btn btn-primary",
      data: { loading: true },
    )
  end
end
# helpers/layout_helper.rb
module LayoutHelper
  def icon_tag(name, extra_class = nil)
    lucide_icon(name, class: ["icon", extra_class].compact.join(" "))
  end

  def icon_with_spinner(name)
    idle = content_tag(:span, icon_tag(name), data: { icon: "idle" })
    spinner = content_tag(:span, icon_tag("loader-circle", "icon-spin"), class: "hidden", data: { icon: "loading" })
    idle + spinner
  end

  def link_to_with_icon(icon, title, url, options = {})
    title_with_icon = icon_with_spinner(icon) << " ".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 loading
# assets/javascripts/loading.coffee
$ ->
  # Insert a spinner and disable the button
  $(document).on('click', 'a[data-loading]', (e) ->
    $(this).attr('disabled', 'disabled')
    $(this).find('[data-icon=idle]').addClass('hidden')
    $(this).find('[data-icon=loading]').removeClass('hidden')
  )
<%# views/layouts/application.html.erb %>
<%= link_to_log_out %>
/* views/sessions/destroy.js.erb */
Turbolinks.visit("<%= new_session_path %>");

Wrap-Up

The Rails code is rather boring, but two important user-experience concepts occur on the frontend:

  1. Disabling the button prevents duplicate submissions.
  2. Using a loading spinner lets the user know we started performing the action.

For further reading, my AJAX Toggle Buttons article builds on these concepts to implement an on/off feature for buttons.

Dan Cunning

Dan Cunning

Ruby on Rails contractor from Atlanta GA, focusing on simplicity and usability through solid design.