Update a Progress Bar using Turbo Streams (using Custom Actions)

When Turbo Streams was announced they allowed you to do a handful actions, like prepend, replace, focusing on your HTML.

Fast-forward, and through this PR it’s now possible to create your own actions. This means everything you can normally do with JavaScript you can do using Turbo Streams.

There is this great article by Marco Roth that explains the basics—check it out.

Setting the stage

I have a report feature, that gathers data, does some calculations and then creates a PDF off of it.

This is some intensive data wrangling, so it makes sense to put it into one or many background jobs. For a great UX the goal is to let the user know about the progress.

What is needed:

  • report model (status: %w[pending ready]);
  • a stimulus controller to show the progress (1..100%);
  • turbo stream, with custom action, to update the progress in the view.

This article will not cover every single detail to get this functionality up and running from zero, but only touches upon the important bits needed for the requirements.

Once the report is requested by the user, respond with a turbo_stream and inject a progress bar.

turbo_stream.prepend("reports", partial: "reports/in_progress")

This will add the reports/_in_progress.html.erb add top of the #reports element.

  <h3>Fasten your seatbelt! We're turbo-charging a dazzling data display just for you.</h3>

  <span data-controller="progress-bar" data-progress-bar-amount-value="0" id="progress_bar" class="block w-0 h-2 bg-blue-500 rounded transition-all"></span>

This needs a Stimulus controller to update the progress bar. Let’s create it.

// app/javascript/controllers/progress_bar_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static values = { amount: Number }

  connect() {

// private

  amountValueChanged() {

  #updateProgress() {
    this.element.style.width = `${this.amountValue}%`;

All this Stimulus controller does is change the width of its element whenever the data-progress-bar-amount-value changes. This is because of the amountValueChanged() function.

Now in each step of the report creation, fire off a turbo_stream to update the amount data attribute.

Let’s imagine the following object:

# app/models/report/creator.rb
class Report::Creator
  def initialize(report)
    @report = report

def create
	# …


def collect
  # collect data

  update_progress(amount: 1)

def wrangle
  # wrangle data

  update_progress(amount: 10)

# etc.

def finish!

  update_progress(amount: 100)
  # TODO: probably update the screen with button to download the report

def update_progress(amount:)
  action = turbo_stream_action_tag(:set_dataset_attribute, value: value)

  ActionCable.server.broadcast("reports", action)

In each “step” the update_progress method is called. This method needs some explanation.

First it uses set_dataset_attribute from turbo-power. It’s a great add-on to your Turbo-powered Rails app.

It then broadcasts the created action to the “reports” stream (make sure you add it!). I’ve not stumbled upon another working solution, but am curious if you know of any.

And these are all the high-level steps needed to get a great UX for long-running background jobs. I think it’s pretty great this is now possible with almost no JavaScript written!

Rails Designer is a professional UI components library for Rails

Get product updates, insights and latest articles in your inbox

Published at . Have suggestions or improvements on this content? Do reach out. Interested in sharing Rails Designer with the Ruby on Rails community? Become an affiliate.

UI components for Ruby on Rails apps

$ 99 one-time

Get Access
  • One-time payment

  • Access to the entire library

  • Built for Ruby on Rails

  • Designed with Tailwind CSS and enhanced with Hotwire

  • Includes free updates (to any 1.x version)