Disabled Button with Bootstrap Spinner in Rails

To construct meaningful user experiences we want to ensure that views provide feedback related to state changes in our software the system. Let's find out how to add an in-progress state for a long running action in Rails. This post assumes you have already added Bootstrap to your application. If not, see this article.

For our application, assume we have a Note model with a state column of type integer which we use a Rails enum for:

class Note < Application Record
  enum state [:started, :pending, :archived]

  def archive!
    sleep(10)
    archived!
  end
end

As shown above, calling note.archive! method will result in a long running process that transitions the note from its current state to archived. We want to demonstrate to the user such that when they click the archive button they receive a spinner while the operation is ongoing.

To do this, we need to update our notes/show.html with the correct logic and requests.

The NotesController archive action is shown in the code snippet below. When the archive action is called, the controller invokes the long running archive! operation and responds to the request.

class NotesController < ApplicationController
  before_action :set_note, only: [:archive]
  
  def archive 
    @note.archive!

    respond_to do |format|
      format.html { redirect_to @note, notice: "Note was archived" }
    end
  end
  
  private
  def set_note
    @note = Note.find(params[:id])
  end
end

To ensure we don't block the UI, we will update our  notes/show.html.erb to make a remote AJAX request and disable the appropriate components using conditional rendering logic for the spinner:

<!-- snipped note HTML -->

<% if display_in_progress?(@note) %> 
  <%= loading_spinner_div %> Action in progress...
<% else %>
  <%= button_to 'Archive', note_archive_path(@note), 
    method: :post,
    class: 'btn btn-info', 
    remote: true, 
    data: { confirm: "Are you sure?", disable_with: loading_spinner_div} unless @note.archived? %> 
<% end %>

In order to make this work, we need to add the two helper methods we added above to our application's NotesHelper module.

module NotesHelper
 def display_in_progress?(note)
  note.pending? 
 end

 def loading_spinner_div 
  content_tag(:div, class: "spinner-grow spinner-grow-sm", role: "status") do
   content_tag(:span, class: "sr-only") do 
     "Working..."
   end
  end
 end
end

The display_in_progress? method captures and returns a boolean to indicate if a set of UI elements should be displayed. The second method loading_spinner_div ❷ returns a reusable HTML element that is styled with Bootstrap styling to display an animated loading dot to indicate that something is happening.

When the user clicks the "Archive" button, they will issue a remote AJAX request to the controller and see a disabled button with a spinner now.