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

[RubyConf China 2021] HTML-over-WebSockets: Fro...

[RubyConf China 2021] HTML-over-WebSockets: From LiveView to Hotwire

Vladimir Dementyev

December 04, 2021
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

  1. HTML (Haml/Slim) Asset Pipeline CoffeeScript jquery Helpers jquery-ujs Turbolinks Sass

    Bootstrap Bundler (asset gems) vendor/assets ES6 Webpack PostCSS React SPA API npm / yarn
  2. YES

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

    process via WebSocket Process reacts on user interaction and re-renders the affected template parts and sends to the client Client uses morphdom to perform a fast DOM patching 13
  4. palkan_tula palkan Partial HTML updates 16 <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>
  5. palkan_tula palkan Partial HTML updates 17 <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" }
  6. 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 22
  7. palkan_tula palkan Example 23 <!-- _item.html.erb --> <div id="<%= dom_id(item)

    %>"> ... <%= button_to item_path(item), method: :delete, remote: true do %> <svg> ... </svg> <% end %> </div>
  8. palkan_tula palkan Example 24 # 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
  9. palkan_tula palkan Example 25 # 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 $(" ##{dom_id(item)}").remove()
  10. 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 27
  11. palkan_tula palkan Example: jQuery 30 🍜 function initBannerClose(){
 // Oops,

    leaking CSS $('.banner --close').click(function(e){ e.preventDefault(); const banner = $(this).parent(); banner.remove(); }); }); $(document).on('load', initBannerClose); // And don't forget about Turbolinks $(document).on('turbolinks:load', initBannerClose); // ...or jquery-ujs $(document).on('ajax:success', initBannerClose);
  12. palkan_tula palkan Example: Stimulus 31 import { Controller } from

    "stimulus"; export class BannerController extends Controller { hide() { this.element.remove(); } }
  13. palkan_tula palkan Stimulus Stimuli are activated/deactivated automatically (MutationObserver API) Just

    drop an element with `data-controller` onto a page (like Custom Elements) 32
  14. palkan_tula palkan StimulusReflex 34 https://my-reflex.app Browser events ➝ reflex class

    & action CableReady operation Action Cable broadcast from anywhere Reflex class DOM element Document
  15. palkan_tula palkan Example 36 <!-- _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>
  16. palkan_tula palkan Example 37 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
  17. palkan_tula palkan Example 38 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
  18. palkan_tula palkan Example 39 class ApplicationReflex < StimulusReflex ::Reflex private

    def morph_flash(type, message) morph "#flash", render_partial( "shared/alerts", {flash: {type => message}} ) end end
  19. 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 40
  20. palkan_tula palkan Turbo Streams Minimalistic CableReady (only 5 actions) Transport-agnostic

    Zero JavaScript (uses custom HTML elements to trigger updates) 48
  21. palkan_tula palkan DOM update Turbo Streams 49 https://my-hot.app <turbo-stream-source> GET/POST/...

    HTML Action Cable broadcast from anywhere Rails controller Action Cable subscribe <turbo-stream>
  22. palkan_tula palkan Turbo Streams 50 <!-- 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>
  23. palkan_tula palkan Turbo Streams 51 # 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
  24. palkan_tula palkan Live 55 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
  25. palkan_tula palkan HTML-over-WebSockets Gives full-stack development a second chance There

    are plenty of implementations already StimulusReflex and CableReady are rock solid! Hotwire is gaining popularity (and stability) 56
  26. palkan_tula palkan HTML-over-WebSockets 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 57