Update a Progress Bar using Turbo Streams (using Custom Actions)
Tags: rails, ruby, hotwire, webdev
Published: 2024-06-06 09:00:00

This article was originally published on [Rails Designer](https://railsdesigner.com/progress-bar-turbo/)

---

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](https://github.com/hotwired/turbo/pull/479) 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](https://marcoroth.dev/posts/guide-to-custom-turbo-stream-actions) 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](#setting-the-stage).

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

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

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

```erb
<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>
```

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

```js
// 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}%`;
  }
}
```

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:

```ruby
# 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
```

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

First it uses `set_dataset_attribute` from [turbo-power](https://github.com/marcoroth/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**!