Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Showing progress of background jobs with Turbo ...

Showing progress of background jobs with Turbo and Stimulus

Demonstration how to broadcast progress of background jobs to frontend.
Presentation with lots of Chuck Norris jokes!

Michal L

June 18, 2024
Tweet

More Decks by Michal L

Other Decks in Programming

Transcript

  1. The app Two models: Joke belongs to JokeRequest Job fetching

    number of jokes from Chuck Norris API “Chuck Norris doesn't have a watch, HE decides what time it is.”
  2. 1. Broadcast from model class class Joke Joke < <

    ApplicationRecord ApplicationRecord after_create_commit after_create_commit -> ->( (joke joke) ) do do broadcast_replace_to broadcast_replace_to([ ([ joke joke. .jokes_request jokes_request, , "jokes_progress_bar" "jokes_progress_bar" ], ], target: target: "jokes_progress_bar" "jokes_progress_bar", , partial: partial: "jokes_requests/jokes_progress_bar" "jokes_requests/jokes_progress_bar", , locals: locals: { { jokes_request: jokes_request: joke joke. .jokes_request jokes_request } } ) ) end end belongs_to belongs_to :jokes_request :jokes_request end end
  3. # app/views/jokes_requests/_jokes_progress_bar.html.erb # app/views/jokes_requests/_jokes_progress_bar.html.erb <% <% progress_width progress_width = =

    jokes_request jokes_request. .jokes jokes. .count count / / jokes_request jokes_request. .amount amount. .to_f to_f * * 100 100 %> %> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> < <div div class class= ="h-0.5 bg-lime-500 rounded-full" "h-0.5 bg-lime-500 rounded-full" style style= ="width: <%= progress_width.to_i %>%;" "width: <%= progress_width.to_i %>%;"></ ></div div> > </ </div div> > 1. Broadcast from model # app/views/jokes_requests/show.html.erb # app/views/jokes_requests/show.html.erb <%= <%= turbo_stream_from turbo_stream_from @jokes_request @jokes_request, , "jokes_progress_bar" "jokes_progress_bar" %> %> # (..) # (..) <%= <%= render render "jokes_progress_bar" "jokes_progress_bar", , jokes_request jokes_request: : @jokes_request @jokes_request %> %>
  4. PROS Fast Easy CONS Callbacks Polluting model CRUD operations with

    broadcasts Not always applicable 1. Broadcast from model Chuck Norris can nail a hammer into a wall.
  5. 2. Broadcast from worker # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def

    update_progress_bar update_progress_bar( (number number) ) Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_replace_to broadcast_replace_to( ( [ [ jokes_request jokes_request, , "jokes_progress_bar" "jokes_progress_bar" ], ], target: target: "jokes_progress_bar" "jokes_progress_bar", , partial: partial: "jokes_requests/jokes_progress_bar" "jokes_requests/jokes_progress_bar", , locals: locals: { { actual: actual: number number, , limit: limit: jokes_request jokes_request. .amount amount } } ) ) end end
  6. 2. Broadcast from worker # app/views/jokes_requests/_jokes_progress_bar.html.erb # app/views/jokes_requests/_jokes_progress_bar.html.erb <% <%

    progress_width progress_width = = actual actual / / limit limit. .to_f to_f * * 100 100 %> %> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> <div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full"> < <div div class class= ="h-0.5 bg-lime-500 rounded-full" "h-0.5 bg-lime-500 rounded-full" style style= ="width: <%= progress_width.to_i %>%;" "width: <%= progress_width.to_i %>%;"></ ></div div> > </ </div div> > # app/views/jokes_requests/show.html.erb # app/views/jokes_requests/show.html.erb <%= <%= turbo_stream_from turbo_stream_from @jokes_request @jokes_request, , "jokes_progress_bar" "jokes_progress_bar" %> %> # (..) # (..) <%= <%= render render "jokes_progress_bar" "jokes_progress_bar", , actual actual: : @jokes_request @jokes_request. .jokes jokes. .count count, , limit limit: : @jokes_request @jokes_request. .amount amount %> %>
  7. PROS No callbacks Easy to test Broadcast limited to one

    source place CONS More work? Still needs to bind to some record/ unique user ID 2. Broadcast from worker
  8. 3. Pseudo-background job # app/views/jokes_request/show.html.erb # app/views/jokes_request/show.html.erb <% <% jokes_request

    jokes_request. .amount amount. .times times do do %> %> <%= turbo_frame_tag "joke", src: fetch_joke_path do %> <%= turbo_frame_tag "joke", src: fetch_joke_path do %> <% <% end end %> %> <% end %> <% end %>
  9. 3. Pseudo-background job # app/controllers/jokes_controller.rb # app/controllers/jokes_controller.rb def def fetch_joke

    fetch_joke @joke @joke = = FetchJokeService FetchJokeService. .new new. .call call end end # app/views/jokes/fetch_joke.html.erb # app/views/jokes/fetch_joke.html.erb <%= <%= turbo_frame_tag turbo_frame_tag "joke" "joke" do do %> %> <div class="bg-slate-200 text-slate-700 text-center text-clip overflow-clip py-4 mb-4 shadow-md <div class="bg-slate-200 text-slate-700 text-center text-clip overflow-clip py-4 mb-4 shadow-md rounded px-4"> rounded px-4"> <%= <%= @joke @joke %> %> </div> </div> <% <% end end %> %>
  10. 4. Updates with Stimulus <!-- app/views/jokes_requests/show.html.erb --> <!-- app/views/jokes_requests/show.html.erb -->

    <!-- just one stream is needed now --> <!-- just one stream is needed now --> < <% %= = turbo_stream_from turbo_stream_from @jokes_request, @jokes_request, "jokes" "jokes" % %> > < <div div id id= ="jokes_show" "jokes_show" data-controller data-controller= ="progress-bar" "progress-bar" data-progress-bar-limit-value data-progress-bar-limit-value= ="<%= @jokes_request.amount %>" "<%= @jokes_request.amount %>" data-progress-bar-actual-value data-progress-bar-actual-value= ="<%= @jokes_request.jokes.size %>" "<%= @jokes_request.jokes.size %>"> > <!-- rest of the html --> <!-- rest of the html --> <!-- (...) --> <!-- (...) --> < <% %= = render render "jokes_progress_bar" "jokes_progress_bar", , actual: actual: @jokes_request.jokes.size, @jokes_request.jokes.size, limit: limit: @jokes_request.amount @jokes_request.amount % %> > < <% %= = turbo_frame_tag turbo_frame_tag "jokes" "jokes" do do % %> > < <div div id id= ='jokes_grid' 'jokes_grid' data-progress-bar-target data-progress-bar-target= ="jokesGrid" "jokesGrid" class class= ="grid grid-cols-3 gap-4 mt-4" "grid grid-cols-3 gap-4 mt-4"> > < <% % @jokes.each @jokes.each do do |joke| |joke| % %> > < <% %= = render render 'jokes/joke' 'jokes/joke', , joke: joke: joke joke % %> > < <% % end end % %> > </ </div div> > < <% % end end % %> >
  11. 4. Updates with Stimulus // app/javascript/controllers/progress_bar_controller.js // app/javascript/controllers/progress_bar_controller.js import import

    { { Controller Controller } } from from "@hotwired/stimulus" "@hotwired/stimulus" // Connects to data-controller="progress-bar" // Connects to data-controller="progress-bar" export export default default class class extends extends Controller Controller { { static static values values = = { { limit limit: : 0 0, , actual actual: : 0 0, , } } static static targets targets = = [ ["progress" "progress", , "count" "count"] ] connect connect() { () { // (...) // (...) } } } }
  12. 4. Updates with Stimulus export export default default class class

    extends extends Controller Controller { { // (..) // (..) connect connect() { () { addEventListener addEventListener( ("turbo:before-stream-render" "turbo:before-stream-render", (( , ((event event) ) => => { { const const fallbackToDefaultActions fallbackToDefaultActions = = event event. .detail detail. .render render event event. .detail detail. .render render = = ( (streamElement streamElement) ) => => { { if if ( (streamElement streamElement. .action action === === "append" "append" && && streamElement streamElement. .target target === === "jokes_grid" "jokes_grid") { ) { this this. .increment increment() () } } fallbackToDefaultActions fallbackToDefaultActions( (streamElement streamElement) ) } } })) })) } } } }
  13. 4. Updates with Stimulus export export default default class class

    extends extends Controller Controller { { increment increment() { () { this this. .actualValue actualValue++ ++ this this. .updateProgress updateProgress() () this this. .updateCount updateCount() () } } updateProgress updateProgress() { () { let let progress progress = = ( (this this. .actualValue actualValue / / this this. .limitValue limitValue) ) * * 100 100 this this. .progressTarget progressTarget. .style style. .width width = = `${ `${progress progress}%` }%` } } updateCount updateCount() { () { this this. .countTarget countTarget. .innerText innerText = = `${ `${this this. .actualValue actualValue} } / ${ / ${this this. .limitValue limitValue}` }` } } } }
  14. Sometimes you just need to move more logic to the

    frontend 4. Updates with Stimulus
  15. Bonus: PAGINATION # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb class class FetchJokesService FetchJokesService

    # ... # ... def def add_joke add_joke( (joke joke, , jokes_count jokes_count) ) last_page last_page = = jokes_count jokes_count / / Joke::PER_PAGE Joke::PER_PAGE + + 1 1 if if jokes_count jokes_count % % Joke::PER_PAGE Joke::PER_PAGE == == 1 1 # change page to next one # change page to next one clear_all_jokes clear_all_jokes add_joke_card add_joke_card( (joke joke) ) replace_pagination replace_pagination( (jokes_count jokes_count, , last_page last_page) ) else else add_joke_card add_joke_card( (joke joke) ) end end end end end end
  16. Bonus: PAGINATION # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def clear_all_jokes clear_all_jokes

    Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_replace_to broadcast_replace_to( ( [ [ jokes_request jokes_request, , "jokes" "jokes" ], ], target: target: "jokes_grid" "jokes_grid", , partial: partial: "jokes_requests/empty_jokes_grid" "jokes_requests/empty_jokes_grid" ) ) end end # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def add_joke_card add_joke_card( (joke joke) ) Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_append_to broadcast_append_to( ( [ [ jokes_request jokes_request, , "jokes" "jokes" ], ], target: target: "jokes_grid" "jokes_grid", , partial: partial: "jokes/joke" "jokes/joke", , locals: locals: { { joke: joke: joke joke } } ) ) end end
  17. Bonus: PAGINATION # app/services/fetch_jokes_service.rb # app/services/fetch_jokes_service.rb def def replace_pagination replace_pagination(

    (jokes_count jokes_count, , last_page last_page) ) pagy pagy = = Pagy Pagy. .new new( (count: count: jokes_count jokes_count, , page: page: last_page last_page, , items: items: Joke::PER_PAGE Joke::PER_PAGE, , link_extra: link_extra: 'data-turbo-action="advance"' 'data-turbo-action="advance"') ) Turbo::StreamsChannel Turbo::StreamsChannel. .broadcast_replace_to broadcast_replace_to( ( [ [ jokes_request jokes_request, , "jokes_pagination" "jokes_pagination" ], ], target: target: "jokes_pagination" "jokes_pagination", , partial: partial: "jokes_requests/jokes_pagination" "jokes_requests/jokes_pagination", , locals: locals: { { pagy: pagy: pagy pagy } } ) ) end end