Resources

Download Source Code

Summary

# Terminal
rails g controller welcome index
rails g controller process index
rails g job progress_increment
rails g channel progress
yarn add stimulus

# view/welcome/index.html.erb
<%= link_to 'Start Large Process', process_index_path, id: 'large-process', class: 'btn btn-primary', remote: true %>

# controllers/process_controller.rb
class ProcessController < ApplicationController
  def index
    @id = (Time.now.to_f * 1000).to_i
    ProgressIncrementJob.perform_later(@id)
  end
end

# views/process/index.js.erb
document.getElementById('large-process').insertAdjacentHTML('afterend', "<%= j render 'progress_bar', id: @id %>")
document.getElementById('large-process').remove()

# config/application.rb
config.active_job.queue_adapter = :async
# in production, you may have something like = Rails.env.production? ? :sidekiq : :async

# jobs/progress_increment_job.rb
class ProgressIncrementJob < ApplicationJob
  queue_as :default

  def perform(id)
    10.times do |i|
      sleep rand() # Some heavy task
      ActionCable.server.broadcast("progress_#{id}", "#{(i + 1) * 10}%")
    end
  end
end

# channels/progress_channel.rb
class ProgressChannel < ApplicationCable::Channel
  def subscribed
    stream_from "progress_#{params[:id]}" if params[:id]
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

# views/process/_progress_bar.html.erb
<%= content_tag :div, class: 'progress', data: { controller: 'progress-bar' } do %>
  <%= tag :div, class: 'progress-bar', id: id, data: { target: 'progress-bar.progress_bar' } %>
<% end %>

# javascript/packs/application.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("../controllers", true, /\.js$/)
application.load(definitionsFromContext(context))

# javascript/controllers/progress_bar_controller.js
import { Controller } from "stimulus"
import consumer from '../channels/consumer'

export default class extends Controller {
  static targets = ['progress_bar']

  initialize() {
    console.log('hello from progress bar controller')
    var progressBar = this.progress_barTarget
    consumer.subscriptions.create({ channel: 'ProgressChannel', id: this.progress_bar_id() }, {
      received(data) {
        progressBar.style.width = data
      }
    })
  }

  progress_bar_id() {
    return this.progress_barTarget.getAttribute('id')
  }  
}

# assets/stylesheets/application.css
.progress {
  background-color: black;
  border-radius: 13px;
  padding: 3px;
}

.progress > .progress-bar {
  background-color: orange;
  width: 0%;
  height: 20px;
  border-radius: 10px;
}