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

[RailsConf 2021] Frontendless Rails Frontend

[RailsConf 2021] Frontendless Rails Frontend

Everything is cyclical, and web development is not an exception: ten years ago, we enjoyed developing Rails apps using HTML forms, a bit of AJAX, and jQuery—our productivity had no end! As interfaces gradually became more sophisticated, the "classic" approach began to give way to frontend frameworks, pushing Ruby into an API provider's role.

The situation started to change; the "new wave" is coming, and ViewComponent, StimulusReflex, and Hotwire are riding the crest.

In this talk, I'd like to demonstrate how we can develop modern "frontend" applications in the New Rails Way.

52cc8a838bf44a589d2572833b2dd1b9?s=128

Vladimir Dementyev

April 12, 2021
Tweet

Transcript

  1. Frontendless Rails Frontend Vladimir Dementyev Evil Martians

  2. palkan_tula palkan github.com/palkan 2

  3. palkan_tula palkan Web development today 3 This is me 😎

  4. palkan_tula palkan Back in 2010s 4 Full-stack developer It's me

    again 🙂
  5. palkan_tula palkan 5 Full-stack Ruby on Rails development in 202😷s

    — is that a thing? The question
  6. palkan_tula palkan 6 Single engineer is enough to screw in

    a light bulb Full-stack is ... 🤔
  7. palkan_tula palkan 7 Developing within a single ecosystem Full-stack is

    ... ☝
  8. Once upon a time...

  9. palkan_tula palkan Full-stack Rails 9 HTML-over-the-Wire

  10. HTML (Haml/Slim) Helpers

  11. palkan_tula palkan Helpers 🙀 11 https://github.com/redmine/redmine

  12. HTML (Haml/Slim) CoffeeScript jquery Helpers jquery-ujs

  13. palkan_tula palkan jquery-ujs 13 # show.html.slim = link_to "Delete", post_path(post),

    remote: true # destroy.js.erb $("#<%= dom_id(post) %>").remove();
  14. HTML (Haml/Slim) Asset Pipeline CoffeeScript jquery Helpers jquery-ujs Turbolinks Sass

    Bootstrap Bundler (asset gems) vendor/assets
  15. The evolution of frontend

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

    Bootstrap Bundler (asset gems) vendor/assets npm / yarn ES6 Webpack PostCSS React SPA API
  17. palkan_tula palkan Frontend Invasion Leads to separation (now we have

    back-end and front-end engineers) Makes Rails to serve only as an API provider Increases development costs* 17 *Opinions expressed are my own, but I'm fine if you borrow them
  18. palkan_tula palkan 18 Is it possible to develop modern web

    application without stepping out of the Ruby and Rails comfort zone 🤔
  19. palkan_tula palkan 19 Living classics

  20. palkan_tula palkan 20 Neoclassical

  21. palkan_tula palkan hey.com 21

  22. palkan_tula palkan NEW MAGIC hotwire.dev 22

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

  24. palkan_tula palkan 24 ?

  25. palkan_tula palkan 25 Client-side rendering Server-side rendering

  26. palkan_tula palkan 26 Client-side rendering Server-side rendering SPA Turbo Drive

    / Frames
  27. 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 27 ex-Turbolinks
  28. palkan_tula palkan Turbo Frames Turbolinks for page fragments (frames) Lazy

    loading of page parts (plays well with HTTP cache) 28
  29. palkan_tula palkan Example 29

  30. palkan_tula palkan Example 30 # 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
  31. palkan_tula palkan Example 31 <!-- _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>
  32. palkan_tula palkan Architecture Reactivity rails-ujs 32 Client-side rendering Server-side rendering

    Interactivity JS framework SPA Turbo Drive / Frames JS sprinkles
  33. palkan_tula palkan Architecture Reactivity rails-ujs Stimulus 33 Client-side rendering Server-side

    rendering Interactivity JS framework JS sprinkles SPA Turbo Drive / Frames
  34. palkan_tula palkan Stimulus 34 stimulusjs.org

  35. palkan_tula palkan Example 35 Hide-able banners

  36. palkan_tula palkan Example: jQuery 36 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); 🍜
  37. palkan_tula palkan Example: Stimulus 37 <div data-controller="banner"> <svg data-action="click ->banner#hide">

    </svg> <p>AnyWork ... </p> </div> import { Controller } from "stimulus"; export class BannerController extends Controller { hide() { this.element.remove(); } }
  38. palkan_tula palkan Stimulus Stimuli are activated/deactivated automatically (MutationObserver API) Turbo(links)

    just works without any hacks 38
  39. palkan_tula palkan Stimulus Turns static HTML into a component ...which

    should be implemented manually (in JS) ...or not 😎 39
  40. palkan_tula palkan 40 Example Interactive forms with Stimulus and Vue

  41. palkan_tula palkan 41 = form_for @form do |f| // ...

    .field label.label Started at .control data-controller="datetimepicker" data-tz="-04:00" = f.text_field :started_at, required: true .field label.label Location .control data-controller="location-input" = f.text_field :location, placeholder: "Enter event location ..." .field label.switch = f.check_box :waitlist_enabled span.check span.control-label.label Waitlist enabled .field label.label Cover Image .control data-controller="upload" data-url=blob_path(f.model.cover) = f.file_field :cover Example
  42. palkan_tula palkan 42 export class VueInputController extends Controller { connect()

    { var el = this.element; this.vue = new Vue( { el, data() { // Pass data attributes as Vue props return { props: el.dataset }; }, template: '<input class="my-input" v-bind="props" />' } ); } disconnect() { if (this.vue) this.vue.$destroy(); } } Example
  43. palkan_tula palkan Stimulus + Vue 43 github.com/gretchenfitze/stimulus-turbolinks

  44. palkan_tula palkan More examples stimulusconnect.com betterstimulus.com github.com/stimulus-use/stimulus-use 44

  45. Okay, okay, no more JavaScript!

  46. palkan_tula palkan Architecture Reactivity rails-ujs Stimulus 46 Client-side rendering Server-side

    rendering Interactivity JS framework SPA Turbo Drive / Frames JS sprinkles HTML-over-WebSocket
  47. palkan_tula palkan Phoenix LiveView 47

  48. 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 48
  49. palkan_tula palkan “A new way to craft modern, reactive web

    interfaces with Ruby on Rails.” 49
  50. palkan_tula palkan 50 Stimulus Reflex creator CableReady 🤔

  51. 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 51
  52. palkan_tula palkan Example 52 <!-- _item.html.erb --> <div id="<%= dom_id(item)

    %>"> ... <%= button_to item_path(item), method: :delete, remote: true do %> <svg> ... </svg> <% end %> </div>
  53. palkan_tula palkan Example 53 # 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
  54. 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 54 $(" ##{dom_id(item)}").remove()
  55. palkan_tula palkan CableReady 55 cableready.stimulusreflex.com

  56. 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 56
  57. None
  58. palkan_tula palkan Example 58

  59. palkan_tula palkan Example 59 <!-- _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>
  60. palkan_tula palkan Example 60 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
  61. palkan_tula palkan Example 61 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 to all connected clients Show flash-notification to the current user Object representing the current element data attributes
  62. palkan_tula palkan Example 62 class ApplicationReflex < StimulusReflex ::Reflex private

    def morph_flash(type, message) morph "#flash", render_partial( "shared/alerts", {flash: {type => message}} ) end end
  63. palkan_tula palkan StimulusReflex Stable & Mature (v3.4) Comprehensive documentation Active

    Discord community (>1k members) Works with AnyCable out-of-the-box 😉 63
  64. palkan_tula palkan More HTML-over-WS Motion Turbo Streams 64

  65. palkan_tula palkan Motion 65 github.com/unabridged/motion

  66. palkan_tula palkan Turbo Streams Minimalistic CableReady (only 5 actions) Transport-agnostic

    Zero JavaScript 66
  67. palkan_tula palkan Turbo Streams 67 <!-- 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> # 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
  68. palkan_tula palkan 68 Everything is HTML. How to keep it

    under control?
  69. palkan_tula palkan Architecture Reactivity 69 Client-side rendering Server-side rendering Interactivity

    JS framework SPA Turbo Drive / Frames JS sprinkles HTML-over-WebSocket
  70. palkan_tula palkan evilmartians.com/blog evilmartians.com/chronicles/evil-front-part-1 70

  71. palkan_tula palkan 71 Think in components

  72. palkan_tula palkan Architecture Reactivity 72 Client-side rendering Server-side rendering Interactivity

    JS framework SPA Turbo Drive / Frames JS sprinkles HTML-over-WebSocket View Components
  73. palkan_tula palkan View Components 73 partials decorators helpers facades presenters

    builders view components
  74. palkan_tula palkan View Component "Ruby objects that output HTML" View

    Model + template Isolated, testable, reusable Made by GitHub 74 github.com/github/view_component
  75. palkan_tula palkan Example 75 # app/components/button/component.rb class Button ::Component <

    ViewComponent ::Base attr_reader :label, :icon def initialize(label:, icon: nil) @label = label @icon = icon end alias icon? icon end
  76. palkan_tula palkan Example 76 # app/components/button/component.html.erb <button class="btn"> <% if

    icon? %> <i><%= icon %> </i> <% end %> <% == label %> </button>
  77. palkan_tula palkan 77 # some.html.erb <div class="container"> <%= render Button

    ::Component.new(label: "Like", icon: "❤") %> </div> Example
  78. palkan_tula palkan 78 # app/components/like_button.rb class LikeButton < Button ::Component

    def initialize super(label: I18n.t("like"), icon: "❤") end end # some.html.erb <div class="container"> <%= render LikeButton.new %> </div> Example
  79. palkan_tula palkan 79 # test/components/button_test.rb class Button ::ComponentTest < ActiveSupport

    ::TestCase include ViewComponent ::TestHelpers def test_render render_inline Button ::Component.new(label: "Test") assert_selector "button.btn", text: "Test" assert_no_selector "button.btn i" end def test_render_with_icon render_inline Button ::Component.new(label: "Test", icon: "✔") assert_selector "button.btn", text: "Test" assert_selector "button.btn i", text: "✔" end end Example
  80. palkan_tula palkan View Component Plays nicely with Rails (Rails way)

    Faster rendering (up to 10x faster than partials) Preview functionality (similarly to mailers) 80
  81. palkan_tula palkan Preview Component 81

  82. palkan_tula palkan View Component 82 app/ frontend/ components/ banner/ component.rb

    component.html.slim component.css component.js Keep HTML, CSS, JS, etc. together
  83. palkan_tula palkan View Component++ 83 app/ frontend/ components/ chat/ component.rb

    component.html.slim component.css component.js controller.js preview.rb preview.html.slim reflex.rb Keep HTML, CSS, JS, Stimulus controllers, reflexes, previews, etc. together
  84. palkan_tula palkan 84 # app/components/button/component.rb class Button ::Component < ViewComponent

    ::Base option :label option :icon, optional: true alias icon? icon end View Component++
  85. palkan_tula palkan View Component++ 85 A collection of extensions and

    developer toos for ViewComponent github.com/palkan/view_component-contrib
  86. palkan_tula palkan Alternatives Cells hanami-view dry-view komponent elemental_components 86

  87. palkan_tula palkan 87 Client-side rendering Server-side rendering JS framework SPA

    Turbo Drive / Frames JS sprinkles HTML-over-WebSocket View Components CSS-in-JS / PostCSS
  88. palkan_tula palkan CSS after Bootstrap Bulma TailwindCSS Shoelace 88

  89. palkan_tula palkan Mobile-first CSS-only Modular and customizable (via Sass vars)

    Could be enhanced by Vue components (Buefy) 89
  90. palkan_tula palkan 90 Bulma + Stimulus + Buefy

  91. palkan_tula palkan Utility-first (no components, just classes) Components extraction mechanism

    (@apply) Fast prototyping (+playground) Optimized production and development builds (JIT) 91
  92. palkan_tula palkan 92

  93. palkan_tula palkan Web Components Customizable Accessibility 93

  94. palkan_tula palkan 94 <!-- flash/component.html.erb --> <sl-alert type="<%=type%>" duration="3000" closable

    open> <=%=body%> </sl-alert>
  95. palkan_tula palkan What to choose? Bulma—admin dashboards, or you know

    some Vue Shoelace—CRUD, admin dashboards TailwindCSS—user-facing interfaces 95
  96. palkan_tula palkan Custom styles 🤔 96

  97. palkan_tula palkan PostCSS Modules 97 Client-side rendering Server-side rendering JS

    framework SPA Turbo Drive / Frames JS sprinkles HTML-over-WebSocket View Components CSS-in-JS / CSS Modules
  98. palkan_tula palkan Example 98 app/ frontend/ components/ flash_alert/ component.rb component.html.slim

    index.css index.js
  99. palkan_tula palkan Example 99 app/ frontend/ components/ flash_alert/ component.rb component.html.slim

    index.css index.js .container { @apply max-w-sm bg-white border-t-4 rounded-b text-teal-900 px-4 py-3 shadow-md; } .body { @apply text-sm; }
  100. palkan_tula palkan Example 100 app/ frontend/ components/ flash_alert/ component.rb component.html.slim

    index.css index.js <div data-controller="<%= component_name %>" class="<%= class_for("container") %>"> <p class="<%= class_for("body") %>"> <%= body %> </p> </div>
  101. palkan_tula palkan Example 101 <div data-controller="flash_alert" class="c-flash_alert-container"> <p class="c-flash_alert-body">Your notice

    has been created </p> </div>
  102. palkan_tula palkan PostCSS modules 102 module.exports = { plugins: {

    'postcss-modules': { generateScopedName: (name, filename, _css) => { const matches = filename.match(/\/frontend\/components\/?(.*)\/index.css$/); if (!matches || !matches[1]) return name; const component = matches[1].replace("/", " --"); return `c-${component}-${name}`; }, }, } }
  103. palkan_tula palkan #class_for 103 class ApplicationViewComponent < ViewComponent ::Base private

    def component_name @component_name ||= self.class.name.sub(" ::Component", "").underscore.split("/").join(" --") end def class_for(name) "c- #{component_name}- #{name}" end end
  104. In the end Or does it even matter?

  105. palkan_tula palkan HTML-over-WebSocket (StimulusReflex, Turbo Streams, etc) JS sprinkles (Stimulus)

    Fake SPA (Turbo) Component-based architectures (ViewComponent, komponent, etc.) Modern CSS frameworks and tools (Tailwind, Shoelace, Bulma, PostCSS) 105 Frontendless Rails Way
  106. palkan_tula palkan Frontendless Rails Way Could be used instead of

    JS/SPA approach for applications with not-so-tricky UI (dashboards, CRUD-s, etc.) Increases productivity (though not for free) We're just in the beginning of the New Era! 106
  107. THANKS! @palkan @palkan_tula evilmartians.com @evilmartians