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

[RubyBanitsa] HTML-over-WebSockets

[RubyBanitsa] HTML-over-WebSockets

https://rubybanitsa.com/events/79

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.

52cc8a838bf44a589d2572833b2dd1b9?s=128

Vladimir Dementyev

June 04, 2021
Tweet

Transcript

  1. HTML-over- WebSockets Vladimir Dementyev Evil Martians

  2. palkan_tula palkan github.com/palkan 2

  3. palkan_tula palkan Web applications 3

  4. palkan_tula palkan Web applications 4 Data Representation

  5. palkan_tula palkan Breakfast 🥐 5 Data Representation ?

  6. palkan_tula palkan Breakfast 🥬 6 Data Representation ?

  7. Canteen-style Ready-made Predefined set of dishes Wait in line Cheap

    & simple
  8. Classic Rails Ready-made (HotW) Predefined set of responses Wait in

    line (queueing) Cheap & simple
  9. DIY Raw food to cook Special equipment is required Expensive

    though tasty
  10. SPA Raw data to render Frontenders are required Expensive though

    "cool" 😎
  11. A la carte Waiters serve clients concurrently Order once, get

    fully-cooked food in batches Refills without requests The chef is a boss
  12. HTML-over-WS Server serves clients concurrently Subscribe once, get HTML asynchronously

    Live updates w/o requests The server is a boss
  13. palkan_tula palkan 13 Why does everyone try to build yet

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

  15. palkan_tula palkan Frontendless Rails RailsConf 2021 15

  16. palkan_tula palkan Frontendless Rails (live) 16

  17. palkan_tula palkan HTML-over- WebSockets: The overview 17

  18. palkan_tula palkan Phoenix LiveView 18

  19. 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
  20. palkan_tula palkan morphdom 20

  21. palkan_tula palkan LiveView 21 https://my-live.app

  22. palkan_tula palkan LiveView 21 https://my-live.app DOM element

  23. palkan_tula palkan LiveView 21 https://my-live.app Erlang process DOM element

  24. palkan_tula palkan LiveView 21 https://my-live.app Browser events Erlang process DOM

    element
  25. palkan_tula palkan LiveView 21 https://my-live.app Browser events Partial HTML updates

    Erlang process DOM element
  26. palkan_tula palkan LiveView 21 https://my-live.app Browser events Partial HTML updates

    Internal events Erlang process DOM element
  27. palkan_tula palkan LiveView 21 https://my-live.app Browser events Partial HTML updates

    Internal events Erlang process DOM element
  28. 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>
  29. 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
  30. 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
  31. 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" }
  32. palkan_tula palkan Phoenix LiveView Component-driven architecture Erlang ecosystem (processes and

    message passing) Dedicated templating mechanism 24
  33. palkan_tula palkan Back to Ruby 25

  34. palkan_tula palkan “A new way to craft modern, reactive web

    interfaces with Ruby on Rails.” 26
  35. palkan_tula palkan 27 Stimulus Reflex creator

  36. palkan_tula palkan 27 Stimulus Reflex creator CableReady 🤔

  37. 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
  38. 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>
  39. 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
  40. 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()
  41. palkan_tula palkan CableReady 32 cableready.stimulusreflex.com

  42. palkan_tula palkan CableReady v5.0 Custom operations stream_from helper (like in

    Hotwire, see further) 33
  43. 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
  44. None
  45. palkan_tula palkan StimulusReflex 36 https://my-reflex.app

  46. palkan_tula palkan StimulusReflex 36 https://my-reflex.app DOM element

  47. palkan_tula palkan StimulusReflex 36 https://my-reflex.app Browser events ➝ reflex class

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

    & action CableReady operation Reflex class DOM element Document
  49. 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
  50. 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
  51. palkan_tula palkan Example 37

  52. palkan_tula palkan Example 37

  53. 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>
  54. 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
  55. 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
  56. 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
  57. 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
  58. palkan_tula palkan StimulusReflex Stable & Mature (v3.4) Comprehensive documentation Active

    Discord community (>1k members) Works with AnyCable out-of-the-box 😉 43
  59. palkan_tula palkan StimulusReflex v4.0 Transport-agnostic (cables, SSE, message_bus, AJAX) Reliable

    data flow 44
  60. palkan_tula palkan NEW MAGIC hotwire.dev 45

  61. palkan_tula palkan NEW MAGIC hotwire.dev 45

  62. palkan_tula palkan evilmartians.com/blog evilmartians.com/chronicles/hotwire-reactive-rails-with-no-javascript 46

  63. palkan_tula palkan Hotwire Demystified @jamie_gaskings at RailsConf 2021 47

  64. 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
  65. palkan_tula palkan Turbo Frames Turbolinks for page fragments (frames) Lazy

    loading of page parts (plays well with HTTP cache) 49
  66. 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
  67. 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>
  68. palkan_tula palkan Turbo Streams Minimalistic CableReady (only 5 actions) Transport-agnostic

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

  70. palkan_tula palkan Turbo Streams 53 https://my-hot.app <turbo-stream-source>

  71. palkan_tula palkan Turbo Streams 53 https://my-hot.app Action Cable subscribe

  72. palkan_tula palkan Turbo Streams 53 https://my-hot.app GET/POST/... Rails controller Action

    Cable subscribe
  73. palkan_tula palkan Turbo Streams 53 https://my-hot.app GET/POST/... HTML Rails controller

    Action Cable subscribe <turbo-stream>
  74. 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>
  75. 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
  76. 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>
  77. 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
  78. palkan_tula palkan More HTML-over-WS Motion Live 56

  79. palkan_tula palkan Motion 57 github.com/unabridged/motion

  80. palkan_tula palkan Live 58 github.com/socketry/live

  81. 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
  82. 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
  83. palkan_tula palkan Motion / Live Conceptually closer to LiveView (stateful

    components) Does keeping view components state scale in Ruby? 🤔 61
  84. 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
  85. 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
  86. THANKS! @palkan @palkan_tula evilmartians.com @evilmartians https://discord.gg/stimulus-reflex