$30 off During Our Annual Pro Sale. View Details »

[RubyBanitsa] HTML-over-WebSockets

[RubyBanitsa] HTML-over-WebSockets

https://rubybanitsa.com/events/79

https://www.youtube.com/watch?v=DfL22z8FjQc&t=12s

The HTML-over-WebSockets approach is conquering the Ruby and Rails universe. New talks, blog posts, frameworks and libraries are popping out like mushrooms in the rain. Want to join this new wave and don't know where to start? My talk is for you!

I'd like to introduce the HTML-over-WebSockets approach, discuss the available architectures (from and outside the Ruby world), and help you figure out whether this new magic fits your needs.

Vladimir Dementyev

June 04, 2021
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

  1. A la carte Waiters serve clients concurrently Order once, get

    fully-cooked food in batches Refills without requests The chef is a boss
  2. palkan_tula palkan 13 Why does everyone try to build yet

    another Korean BBQ? Is HTML-over-WebSockets possible in 2021? The question
  3. YES

  4. palkan_tula palkan Phoenix LiveView HTML elements «connects» to an Erlang

    process via WebSocket Process reacts on user interaction and internal server events, updates the state, re-renders the affected template parts and sends to the client Client uses morphdom to perform a fast DOM patching 19
  5. palkan_tula palkan Partial HTML updates 22 <div class="item <%= item.completed?

    ? "checked" : "" %>" id="<%= dom_id(item) %>"> <label class="checkbox"> <input type="checkbox" class="hidden" <%= item.completed? ? "checked" : "" % >> </label> <p><%= item.desc %> </p> </div>
  6. palkan_tula palkan Partial HTML updates 23 <div class="item <%= item.completed?

    ? "checked" : "" %>" id="<%= dom_id(item) %>"> <label class="checkbox"> <input type="checkbox" class="hidden" <%= item.completed? ? "checked" : "" % >> </label> <p><%= item.desc %> </p> </div> Static vs. Dynamic
  7. palkan_tula palkan Partial HTML updates 23 <div class="item <%= item.completed?

    ? "checked" : "" %>" id="<%= dom_id(item) %>"> <label class="checkbox"> <input type="checkbox" class="hidden" <%= item.completed? ? "checked" : "" % >> </label> <p><%= item.desc %> </p> </div> Static vs. Dynamic 0 1 2 3
  8. palkan_tula palkan Partial HTML updates 23 <div class="item <%= item.completed?

    ? "checked" : "" %>" id="<%= dom_id(item) %>"> <label class="checkbox"> <input type="checkbox" class="hidden" <%= item.completed? ? "checked" : "" % >> </label> <p><%= item.desc %> </p> </div> Static vs. Dynamic 0 1 2 3 { "0": "checked", "2": "checked" }
  9. palkan_tula palkan CableReady A library to broadcast DOM modification commands

    from server to browsers Uses Action Cable as a transport Uses morphdom to update HTML 28
  10. palkan_tula palkan Example 29 <!-- _item.html.erb --> <div id="<%= dom_id(item)

    %>"> ... <%= button_to item_path(item), method: :delete, remote: true do %> <svg> ... </svg> <% end %> </div>
  11. palkan_tula palkan Example 30 # items_controller.rb def destroy item.destroy! stream

    = ListChannel.broadcasting_for(item.list) cable_ready[stream].remove(selector: dom_id(item)) head :no_content end
  12. palkan_tula palkan # items_controller.rb def destroy item.destroy! stream = ListChannel.broadcasting_for(item.list)

    cable_ready[stream].remove(selector: dom_id(item)) head :no_content end Example 31 $(" ##{dom_id(item)}").remove()
  13. palkan_tula palkan StimulusReflex Reflexes react on user actions and render

    HTML responses CableReady is use to send HTML to clients and to update DOM 34
  14. palkan_tula palkan StimulusReflex 36 https://my-reflex.app Browser events ➝ reflex class

    & action CableReady operation Reflex class DOM element Document
  15. palkan_tula palkan StimulusReflex 36 https://my-reflex.app Browser events ➝ reflex class

    & action CableReady operation Action Cable broadcast from anywhere Reflex class DOM element Document
  16. palkan_tula palkan StimulusReflex 36 https://my-reflex.app Browser events ➝ reflex class

    & action CableReady operation Action Cable broadcast from anywhere Reflex class DOM element Document
  17. palkan_tula palkan Example 38 <!-- _item.html.erb --> <div id="<%= dom_id(item)

    %>"> <label class="any-check mr-4"> <input type="checkbox" class="hidden" <%= item.completed? ? "checked" : "" %> data-reflex="change ->List#toggle_item_completion" data-item-id="<%= item.id %> > <svg> ... </svg> </label> <p><%= item.desc %> </p> <button data-reflex="click ->List#destroy_item" data-item-id="<%= item.id %>"> <svg> ... </svg> </button> </div>
  18. palkan_tula palkan Example 39 class ListReflex < ApplicationReflex def toggle_item_completion

    item = find_item item.toggle!(:completed) html = render_partial("items/item", {item}) selector = dom_id(item) cable_ready[ ListChannel.broadcasting_for(item.list) ].outer_html(selector:, html:) cable_ready.broadcast morph_flash :notice, "Item has been updated" end private def find_item Item.find element.dataset["item-id"] end end
  19. palkan_tula palkan Example 40 class ListReflex < ApplicationReflex def toggle_item_completion

    item = find_item item.toggle!(:completed) html = render_partial("items/item", {item}) selector = dom_id(item) cable_ready[ ListChannel.broadcasting_for(item.list) ].outer_html(selector:, html:) cable_ready.broadcast morph_flash :notice, "Item has been updated" end private def find_item Item.find element.dataset["item-id"] end end Broadcast DOM updates to all connected clients Show flash-notification to the current user Object representing the current element data attributes
  20. palkan_tula palkan Example 41 class ApplicationReflex < StimulusReflex ::Reflex private

    def morph_flash(type, message) morph "#flash", render_partial( "shared/alerts", {flash: {type => message}} ) end end
  21. palkan_tula palkan StimulusReflex Introduces a new abstraction layer (reflexes) Unscoped

    DOM updates (you can even update the whole page) DOM manipulations could be customized to infinity 42
  22. palkan_tula palkan StimulusReflex Stable & Mature (v3.4) Comprehensive documentation Active

    Discord community (>1k members) Works with AnyCable out-of-the-box 😉 43
  23. palkan_tula palkan Turbo Drive Poor man's SPA Intercepts navigation /

    forms submission, performs AJAX requests, replaces HTML body contents Keeps track of visited pages (cache) to provide smooth experience 48 ex-Turbolinks
  24. palkan_tula palkan Turbo Frames Turbolinks for page fragments (frames) Lazy

    loading of page parts (plays well with HTTP cache) 49
  25. palkan_tula palkan Example 50 # app/controllers/items_controller.rb class ItemsController < ApplicationController

    def update item.update!(item_params) render partial: "item", locals: {item} end def destroy item.destroy! render partial: "item", locals: {item} end end
  26. palkan_tula palkan Example 51 <!-- _item.html.erb --> <turbo-frame id="<%=dom_id(item)%>"> <%

    unless item.destroyed? %> <div class="<%=item.completed? ? "checked" : "" %>"> <%= form_for item do |f| %> <label class="any-check mr-4"> <%= f.check_box :completed, class: item.completed? ? "hidden checked" : "hidden", onchange: "this.form.requestSubmit();" %> </label> <% end %> <p><%= item.desc %> </p> <%= button_to item_path(item), method: :delete do %> <svg> ... </svg> <% end %> </div> <% end %> </turbo-frame>
  27. palkan_tula palkan Turbo Streams Minimalistic CableReady (only 5 actions) Transport-agnostic

    Zero JavaScript (uses custom HTML elements to trigger updates) 52
  28. palkan_tula palkan Turbo Streams 53 https://my-hot.app GET/POST/... HTML Action Cable

    broadcast from anywhere Rails controller Action Cable subscribe <turbo-stream>
  29. palkan_tula palkan DOM update Turbo Streams 53 https://my-hot.app GET/POST/... HTML

    Action Cable broadcast from anywhere Rails controller Action Cable subscribe
  30. palkan_tula palkan Turbo Streams 54 <!-- app/views/workspaces/show.html.erb --> <div> <%=

    turbo_stream_from workspace %> <div id="<%= dom_id(workspace, :chat) %>" class="chat"> <h1><%= workspace.name %> </h1> <hr class="mt-1"> <div class="messages" id="<%= dom_id(workspace, :chat_messages) %>"> </div> </div>
  31. palkan_tula palkan Turbo Streams 55 # app/controllers/chat/messages_controller.rb class MessagesController <

    ApplicationController def create Turbo ::StreamsChannel.broadcast_append_to( workspace, target: ActionView ::RecordIdentifier.dom_id(workspace, :chat_messages), partial: "chats/message", locals: {message: params[:message], name: current_user.name} ) head :ok end end
  32. palkan_tula palkan Live 59 class ClickCounter < Live ::View def

    initialize(id, **data) super @data[:count] ||= 0 end def handle(event, details) @data[:count] = Integer(@data[:count]) + 1 replace! end def render(builder) builder.tag :button, onclick: forward do builder.text("Add an image. ( #{@data[:count]} images so far).") end builder.tag :div do Integer(@data[:count]).times do builder.tag :img, src: "https: //picsum.photos/200/300" end end end end
  33. palkan_tula palkan Live 60 class ClickCounter < Live ::View def

    initialize(id, **data) super @data[:count] ||= 0 end def handle(event, details) @data[:count] = Integer(@data[:count]) + 1 replace! end def render(builder) builder.tag :button, onclick: forward do builder.text("Add an image. ( #{@data[:count]} images so far).") end builder.tag :div do Integer(@data[:count]).times do builder.tag :img, src: "https: //picsum.photos/200/300" end end end end
  34. palkan_tula palkan Motion / Live Conceptually closer to LiveView (stateful

    components) Does keeping view components state scale in Ruby? 🤔 61
  35. palkan_tula palkan HTML-over-WebSockets There are plenty of implementations already StimulusReflex

    and CableReady are rock solid! Hotwire is gaining popularity (and stability) Motion and Live are promising 62
  36. palkan_tula palkan HTML-over-WebSockets Gives full-stack development a second chance Worth

    considering if: You don't want to hire the whole new team Your app is based on user-server interactions You want to vitalize a classic Rails app 63