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

[EMEA on Rails] Frontendless Rails

[EMEA on Rails] Frontendless Rails

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.

Vladimir Dementyev

June 09, 2021
Tweet

More Decks by Vladimir Dementyev

Other Decks in Programming

Transcript

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

    remote: true # destroy.js.erb $("#<%= dom_id(post) %>").remove();
  2. 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
  3. 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* 15 *Opinions expressed are my own, but I'm fine if you borrow them
  4. palkan_tula palkan 16 Is it possible to develop modern web

    application without stepping out of the Ruby and Rails comfort zone 🤔
  5. 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 23 ex-Turbolinks
  6. palkan_tula palkan Turbo Frames Turbolinks for page fragments (frames) Lazy

    loading of page parts (plays well with HTTP cache) 24
  7. palkan_tula palkan Example 26 # 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
  8. palkan_tula palkan Example 27 <!-- _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>
  9. palkan_tula palkan Architecture Reactivity rails-ujs 28 Client-side rendering Server-side rendering

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

    rendering Interactivity JS framework JS sprinkles SPA Turbo Drive / Frames
  11. palkan_tula palkan Example: jQuery 32 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 33 <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(); } }
  13. palkan_tula palkan Stimulus Turns static HTML into a component ...which

    should be implemented manually (in JS) ...or not 😎 35
  14. palkan_tula palkan Architecture Reactivity rails-ujs Stimulus 39 Client-side rendering Server-side

    rendering Interactivity JS framework SPA Turbo Drive / Frames JS sprinkles HTML-over-WebSocket
  15. 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 43
  16. palkan_tula palkan Example 44 <!-- _item.html.erb --> <div id="<%= dom_id(item)

    %>"> ... <%= button_to item_path(item), method: :delete, remote: true do %> <svg> ... </svg> <% end %> </div>
  17. palkan_tula palkan Example 45 # 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
  18. 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 46 $(" ##{dom_id(item)}").remove()
  19. 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 49
  20. palkan_tula palkan Example 52 <!-- _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>
  21. palkan_tula palkan Example 53 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
  22. palkan_tula palkan Example 54 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
  23. palkan_tula palkan Example 55 class ApplicationReflex < StimulusReflex ::Reflex private

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

    Discord community (>1k members) Works with AnyCable out-of-the-box 😉 56
  25. palkan_tula palkan More reflexions futurism—lazy-load HTML parts only when they

    become visible optimism—real-time remote form validations 58
  26. palkan_tula palkan Turbo Streams 61 <!-- 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
  27. palkan_tula palkan Architecture Reactivity 66 Client-side rendering Server-side rendering Interactivity

    JS framework SPA Turbo Drive / Frames JS sprinkles HTML-over-WebSocket
  28. palkan_tula palkan Architecture Reactivity 69 Client-side rendering Server-side rendering Interactivity

    JS framework SPA Turbo Drive / Frames JS sprinkles HTML-over-WebSocket View Components
  29. palkan_tula palkan View Component "Ruby objects that output HTML" View

    Model + template Isolated, testable, reusable Made by GitHub 71 github.com/github/view_component
  30. palkan_tula palkan Example 72 # 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
  31. palkan_tula palkan Example 73 # app/components/button/component.html.erb <button class="btn"> <% if

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

    ::Component.new(label: "Like", icon: "❤") %> </div> Example
  33. palkan_tula palkan 75 # 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
  34. palkan_tula palkan 76 # 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
  35. palkan_tula palkan View Component Plays nicely with Rails (Rails way)

    Faster rendering (up to 10x faster than partials) Preview functionality (similarly to mailers) 77
  36. palkan_tula palkan View Component 78 app/ frontend/ components/ banner/ component.rb

    component.html.slim component.css component.js Keep HTML, CSS, JS, etc. together
  37. palkan_tula palkan View Component++ 79 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
  38. palkan_tula palkan View Component++ 80 A collection of extensions and

    developer toos for ViewComponent github.com/palkan/view_component-contrib
  39. palkan_tula palkan 82 Client-side rendering Server-side rendering JS framework SPA

    Turbo Drive / Frames JS sprinkles HTML-over-WebSocket View Components CSS-in-JS / PostCSS
  40. palkan_tula palkan Utility-first (no components, just classes) Components extraction mechanism

    (@apply) Fast prototyping (+playground) Optimized production and development builds (JIT) 86
  41. palkan_tula palkan What to choose? Bulma—admin dashboards, or you know

    some Vue Shoelace—CRUD, admin dashboards TailwindCSS—user-facing interfaces 90
  42. 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) 92 Frontendless Rails Way
  43. 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! 93