Robert's Homepage

How to Create Disabled Spinner Button in Rails

#ruby-on-rails #ruby #how-to

How to Create Disabled Button with Spinner in Rails

To construct meaningful user experiences we want to ensure that views provide feedback related to state changes in system. This post assumes you have already added Bootstrap to your application.

Assume there is a model called Note with a state column of type integer backed by a Rails enum and a NotesController with an archive action called by the view.

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

  def archive!
    sleep(10)
    archived!
  end
end

A note record with title: unarchived note, body: commit, and status: started. A blue archive button below the note values

Figure 1: Clicking the Archive button will cause the long running process to start

The NotesController#archive action is shown in the code snippet below.

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

Calling the NotesController#archive action will invoke the note.archive! method beginning a long running process to transition the note from its current state to the archived state. The desired experience to communicate this to the user by replacing the button contents with a pending spinner while the operation is ongoing.

To prevent UI blocking, we will update the 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.