javascript erb ruby 111 lines · 3 tabs

Progress indicators for long-running operations

Jordan Lee Jan 2026
3 tabs
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["bar", "percent", "status"]
  static values = {
    current: { type: Number, default: 0 },
    total: { type: Number, default: 100 }
  }

  connect() {
    this.updateProgress()
  }

  currentValueChanged() {
    this.updateProgress()
  }

  updateProgress() {
    const percent = Math.round((this.currentValue / this.totalValue) * 100)

    this.barTarget.style.width = `${percent}%`
    this.percentTarget.textContent = `${percent}%`

    if (percent >= 100) {
      this.complete()
    }
  }

  setProgress(event) {
    const { current, total, status } = event.detail

    this.currentValue = current
    this.totalValue = total

    if (status && this.hasStatusTarget) {
      this.statusTarget.textContent = status
    }
  }

  complete() {
    if (this.hasStatusTarget) {
      this.statusTarget.textContent = 'Complete!'
    }

    // Dispatch completion event
    this.element.dispatchEvent(new CustomEvent('progress:complete'))
  }
}
3 files · javascript, erb, ruby Explain with highlit

Users need feedback during slow operations like file uploads or complex processing. I combine Turbo Streams with background jobs to show real-time progress. When an operation starts, I enqueue a job that periodically broadcasts progress updates via Action Cable. The frontend subscribes to a user-specific channel and updates a progress bar as messages arrive. For file uploads, I use ActiveStorage's direct upload with progress events. The key is providing meaningful progress—showing percentage when possible, or indeterminate spinners when progress can't be measured. I also show estimated time remaining and allow cancellation when feasible. Clear progress indicators reduce perceived latency and abandonment rates.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs
ruby
class TasksController < ApplicationController
  def destroy
    @task = Task.find(params[:id])
    @task.destroy!

    respond_to do |format|

Remove deleted items instantly with turbo_stream.remove

rails hotwire turbo
by Henry Kim 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Progress indicators for long-running operations — share card
Link copied