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

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

This article was originally published on Rails Designer


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")
Enter fullscreen mode
Exit fullscreen mode

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

<div> <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>
</div>
Enter fullscreen mode
Exit fullscreen mode

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() { this.#updateProgress(); } // private amountValueChanged() { this.#updateProgress(); } #updateProgress() { this.element.style.width = `${this.amountValue}%`; }
}
Enter fullscreen mode
Exit fullscreen mode

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 end def create collect wrangle # … finish!
end private def collect # collect data update_progress(amount: 1)
end def wrangle # wrangle data update_progress(amount: 10)
end # etc. def finish! @report.ready! update_progress(amount: 100) # TODO: probably update the screen with button to download the report
end def update_progress(amount:) action = turbo_stream_action_tag(:set_dataset_attribute, value: value) ActionCable.server.broadcast("reports", action)
end
Enter fullscreen mode
Exit fullscreen mode

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!


Discover more from Coursity

Subscribe to get the latest posts to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *