DEV Community

loading...
Cover image for Build a server updated async progress bar with Rails in 5 steps
CodeFund

Build a server updated async progress bar with Rails in 5 steps

Hopsoft
Reactive Ruby on Rails & HTML over the wire, creator of CableReady, StimulusReflex, & several other libs, CTO @ MatrixMedia, CodeFund, Skipio, & DMS
・2 min read

This tutorial demonstrates how simple it is to perform DOM updates from Rails background jobs with CableReady.

Progress Bar Demo

Intro

Ruby on Rails supports websockets out of the box via a built in library known as ActionCable. I created a library named CableReady that works with ActionCable to perform common DOM operations from background jobs without requiring you to write any custom JavaScript. And, it's very performant.

1. Create the Rails project

rails new progress_bar_demo
cd progress_bar_demo
Enter fullscreen mode Exit fullscreen mode

2. Create the restful resource

First create the controller and HTML page.

bundle exec rails generate controller progress_bars
touch app/views/progress_bars/show.html.erb
Enter fullscreen mode Exit fullscreen mode
<!-- app/views/progress_bars/show.html.erb -->
<h1>Progress Bar Demo</h1>
<div id="progress-bar">
  <div></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Then update the routes file.

# config/routes.rb
Rails.application.routes.draw do
  resource :progress_bar, only: [:show]
  root "progress_bars#show"
end
Enter fullscreen mode Exit fullscreen mode

3. Setup the styling

First create the stylesheet.

mkdir app/javascript/stylesheets
touch app/javascript/stylesheets/application.scss
Enter fullscreen mode Exit fullscreen mode
// app/javascript/stylesheets/application.scss
#progress-bar {
  background-color: #ccc;
  border-radius: 13px;
  padding: 3px;
}

#progress-bar>div {
  background-color: green;
  width: 0;
  height: 20px;
  border-radius: 10px;
}
Enter fullscreen mode Exit fullscreen mode

Then update the JavaScript pack to include the stylesheet.

// app/javascript/packs/application.js
require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

import "../stylesheets/application.scss" // <-- add this line
Enter fullscreen mode Exit fullscreen mode

Finally, update the application layout to use the stylesheet pack.

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
  <head>
    <title>ProgressBarDemo</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <!-- line below was updated to use stylesheet_pack_tag -->
    <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

4. Setup the ActionCable channel

yarn add cable_ready
bundle exec rails generate channel progress_bar
Enter fullscreen mode Exit fullscreen mode
// app/javascript/channels/progress_bar_channel.js
import consumer from "./consumer"
import CableReady from 'cable_ready'

consumer.subscriptions.create("ProgressBarChannel", {
  received: data => {
    if (data.cableReady) CableReady.perform(data.operations)
  }
})
Enter fullscreen mode Exit fullscreen mode
# app/channels/progress_bar_channel.rb
class ProgressBarChannel < ApplicationCable::Channel
  def subscribed
    stream_from "ProgressBarChannel"
  end
end
Enter fullscreen mode Exit fullscreen mode

5. Setup the backend

bundle add cable_ready
bundle exec rails generate job progress_bar
Enter fullscreen mode Exit fullscreen mode

When this job fires, it runs a loop that fills in the progress bar a little bit on each iteration. This is possible because CableReady allows us to send commands to the browser that update the DOM without the need to write custom Javascript.

# app/jobs/progress_bar_job.rb
class ProgressBarJob < ApplicationJob
  include CableReady::Broadcaster
  queue_as :default

  def perform
    status = 0
    while status < 100
      status += 10
      cable_ready["ProgressBarChannel"].set_attribute(
        selector: "#progress-bar>div",
        name: "style",
        value: "width:#{status}%"
      )
      cable_ready.broadcast
      sleep 1 # fake some latency
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
# app/controllers/progress_bars_controller.rb
class ProgressBarsController < ApplicationController
  def show
    ProgressBarJob.set(wait: 1.second).perform_later
  end
end
Enter fullscreen mode Exit fullscreen mode

6. Run and watch the magic

bundle exec rails s
Enter fullscreen mode Exit fullscreen mode

Then visit http://localhost:3000 in a browser.

Disclaimer

⚠️ This demo is tailored for the development environment. In a production setup you'd need to configure both ActionCable and ActiveJob to use Redis. You'd also want to secure the ActionCable channel.

Discussion (2)

Collapse
darkrubyist profile image
darkrubyist • Edited

Thank you very much @hopsoft for CableReady . I just tried the progress bar tutorial and I think it was the easiest ActionCable progress bar I ever did. I hope you will have more time in the future for more tutorials.

Added transition: .5s to get a smoother progress transition.

enter image description here

As I used Tachyons CSS on my project and I had to add the transition inside the ProgressBarJob as the html content was replaced by the CableReady real-time update.

# app/jobs/progress_bar_job.rb
class ProgressBarJob < ApplicationJob
  ...

  def perform
    status = 0
    while status < 100
      status += 10
      cable_ready["ProgressBarChannel"].set_attribute(
        selector: "#progress-bar>div",
        name: "style",
        value: "width:#{status}%; transition: .5s;"   # <-- this line was changed
      )
      cable_ready.broadcast
      sleep 1 # fake some latency # <-- this line was changed
    end
  end
end

However, the transition can be added directly to the application.scss .

// app/javascript/stylesheets/application.scss
#progress-bar>div {
  background-color: green;
  width: 0;
  height: 20px;
  border-radius: 10px;
  transition: .5s; # <-- this line was changed
}

Update

Progress Messages

After playing some time with CableReady, I wanted to extend the progress bar with a real-time feedback, to help the end user see the current step of the progress bar.

<!-- show.html.erb -->

<h3 id="message"></h3>
...

# app/jobs/progress_bar_job.rb

...

if status < 30
    cable_ready["ProgressBarChannel"].inner_html(
      selector: "#message",
      html: "We are preparing your stuff..."
    )
end

cable_ready.broadcast
sleep 0.1

Collapse
hopsoft profile image
Hopsoft Author

Note that the CableReady docs are pretty terrible. I plan to get them updated soon.