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. Frontendless
    Rails
    Frontend
    Vladimir Dementyev
    Evil Martians

    View Slide

  2. palkan_tula
    palkan
    github.com/palkan
    2

    View Slide

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

    View Slide

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

    View Slide

  5. palkan_tula
    palkan
    5
    Full-stack Ruby on Rails development in 202😷s
    — is that a thing?
    The question

    View Slide

  6. Once upon
    a time...

    View Slide

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

    View Slide

  8. HTML (Haml/Slim)
    Helpers

    View Slide

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

    View Slide

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

    View Slide

  11. palkan_tula
    palkan
    jquery-ujs
    11
    # show.html.slim
    = link_to "Delete", post_path(post), remote: true
    # destroy.js.erb
    $("#<%= dom_id(post) %>").remove();

    View Slide

  12. HTML (Haml/Slim)
    Asset Pipeline
    CoffeeScript
    jquery
    Helpers
    jquery-ujs
    Turbolinks
    Sass
    Bootstrap
    Bundler (asset gems)
    vendor/assets

    View Slide

  13. The evolution
    of frontend

    View Slide

  14. 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

    View Slide

  15. 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

    View Slide

  16. palkan_tula
    palkan
    16
    Is it possible to develop modern web application
    without stepping out of the Ruby and Rails
    comfort zone 🤔

    View Slide

  17. palkan_tula
    palkan
    17
    Living classics

    View Slide

  18. palkan_tula
    palkan
    18
    Neoclassical

    View Slide

  19. palkan_tula
    palkan
    NEW MAGIC
    hotwire.dev
    19

    View Slide

  20. palkan_tula
    palkan
    20
    ?

    View Slide

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

    View Slide

  22. palkan_tula
    palkan
    22
    Client-side rendering
    Server-side rendering
    SPA
    Turbo Drive / Frames

    View Slide

  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
    23
    ex-Turbolinks

    View Slide

  24. palkan_tula
    palkan
    Turbo Frames
    Turbolinks for page fragments (frames)
    Lazy loading of page parts (plays well with HTTP
    cache)
    24

    View Slide

  25. palkan_tula
    palkan
    Example
    25

    View Slide

  26. 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

    View Slide

  27. palkan_tula
    palkan
    Example
    27


    <% unless item.destroyed? %>
    ">
    <%= form_for item do |f| %>

    <%= f.check_box :completed,
    class: item.completed? ? "hidden checked" : "hidden",
    onchange: "this.form.requestSubmit();" %>

    <% end %>
    <%= item.desc %>
    <%= button_to item_path(item), method: :delete do %>
    ...

    <% end %>

    <% end %>

    View Slide

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

    View Slide

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

    View Slide

  30. palkan_tula
    palkan
    Stimulus
    30
    stimulusjs.org

    View Slide

  31. palkan_tula
    palkan
    Example
    31
    Hide-able banners

    View Slide

  32. 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);
    🍜

    View Slide

  33. palkan_tula
    palkan
    Example: Stimulus
    33


    AnyWork ...

    import { Controller } from "stimulus";
    export class BannerController extends Controller {
    hide() {
    this.element.remove();
    }
    }

    View Slide

  34. palkan_tula
    palkan
    Stimulus
    Stimuli are activated/deactivated automatically
    (MutationObserver API)
    Turbo(links) just works without any hacks
    34

    View Slide

  35. palkan_tula
    palkan
    Stimulus
    Turns static HTML into a component
    ...which should be implemented manually (in JS)
    ...or not 😎
    35

    View Slide

  36. palkan_tula
    palkan
    Stimulus + Vue
    36
    github.com/gretchenfitze/stimulus-turbolinks

    View Slide

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

    View Slide

  38. Okay, okay,
    no more JavaScript!

    View Slide

  39. 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

    View Slide

  40. palkan_tula
    palkan
    Phoenix LiveView
    40

    View Slide

  41. palkan_tula
    palkan
    “A new way to craft modern,
    reactive web interfaces with
    Ruby on Rails.”
    41

    View Slide

  42. palkan_tula
    palkan
    42
    Stimulus Reflex creator
    CableReady 🤔

    View Slide

  43. 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

    View Slide

  44. palkan_tula
    palkan
    Example
    44


    ...
    <%= button_to item_path(item),
    method: :delete,
    remote: true do %>
    ...
    <% end %>

    View Slide

  45. 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

    View Slide

  46. 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()

    View Slide

  47. palkan_tula
    palkan
    CableReady
    47
    cableready.stimulusreflex.com

    View Slide

  48. palkan_tula
    palkan
    CableReady v5.0
    Custom operations
    stream_from helper (like in Hotwire, see further)
    48

    View Slide

  49. 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

    View Slide

  50. View Slide

  51. palkan_tula
    palkan
    Example
    51

    View Slide

  52. palkan_tula
    palkan
    Example
    52



    <%= item.completed? ? "checked" : "" %>
    data-reflex="change ->List#toggle_item_completion"
    data-item-id="<%= item.id %>
    >
    ...

    <%= item.desc %>
    data-item-id="<%= item.id %>">
    ...


    View Slide

  53. 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

    View Slide

  54. 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

    View Slide

  55. 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

    View Slide

  56. palkan_tula
    palkan
    StimulusReflex
    Stable & Mature (v3.4)
    Comprehensive documentation
    Active Discord community (>1k members)
    Works with AnyCable out-of-the-box 😉
    56

    View Slide

  57. palkan_tula
    palkan
    StimulusReflex v4.0
    Transport-agnostic (cables, SSE, message_bus,
    AJAX)
    Reliable data flow
    57

    View Slide

  58. palkan_tula
    palkan
    More reflexions
    futurism—lazy-load HTML parts only when they
    become visible
    optimism—real-time remote form validations
    58

    View Slide

  59. palkan_tula
    palkan
    More HTML-over-WS
    Turbo Streams
    Motion
    Live
    59

    View Slide

  60. palkan_tula
    palkan
    Turbo Streams
    Minimalistic CableReady (only 5 actions)
    Transport-agnostic
    Zero JavaScript
    60

    View Slide

  61. palkan_tula
    palkan
    Turbo Streams
    61


    <%= turbo_stream_from workspace %>

    <%= workspace.name %>




    # 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

    View Slide

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

    View Slide

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

    View Slide

  64. palkan_tula
    palkan
    Live
    *copyright / url caption
    64
    github.com/socketry/live

    View Slide

  65. palkan_tula
    palkan
    65
    Everything is HTML. How to keep it under control?

    View Slide

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

    View Slide

  67. palkan_tula
    palkan
    evilmartians.com/blog
    evilmartians.com/chronicles/evil-front-part-1
    67

    View Slide

  68. palkan_tula
    palkan
    68
    Think in components

    View Slide

  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
    View Components

    View Slide

  70. palkan_tula
    palkan
    View Components
    70
    partials
    decorators
    helpers
    facades
    presenters
    builders
    view components

    View Slide

  71. 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

    View Slide

  72. 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

    View Slide

  73. palkan_tula
    palkan
    Example
    73
    # app/components/button/component.html.erb

    <% if icon? %>
    <%= icon %>
    <% end %>
    <% == label %>

    View Slide

  74. palkan_tula
    palkan
    74
    # some.html.erb

    <%= render Button ::Component.new(label: "Like", icon: "❤") %>

    Example

    View Slide

  75. 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

    <%= render LikeButton.new %>

    Example

    View Slide

  76. 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

    View Slide

  77. 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

    View Slide

  78. 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

    View Slide

  79. 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

    View Slide

  80. palkan_tula
    palkan
    View Component++
    80
    A collection of extensions and developer
    toos for ViewComponent
    github.com/palkan/view_component-contrib

    View Slide

  81. palkan_tula
    palkan
    Alternatives
    Cells
    hanami-view
    dry-view
    komponent
    elemental_components
    81

    View Slide

  82. 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

    View Slide

  83. palkan_tula
    palkan
    CSS after Bootstrap
    Bulma
    TailwindCSS
    Shoelace
    83

    View Slide

  84. palkan_tula
    palkan
    Mobile-first
    CSS-only
    Modular and customizable (via Sass vars)
    Could be enhanced by Vue components (Buefy)
    84

    View Slide

  85. palkan_tula
    palkan
    85
    Bulma + Stimulus + Buefy

    View Slide

  86. palkan_tula
    palkan
    Utility-first (no components, just classes)
    Components extraction mechanism (@apply)
    Fast prototyping (+playground)
    Optimized production and development builds
    (JIT)
    86

    View Slide

  87. palkan_tula
    palkan
    87

    View Slide

  88. palkan_tula
    palkan
    Web Components
    Customizable
    Accessibility
    88

    View Slide

  89. palkan_tula
    palkan
    89

    <=%=body%>

    View Slide

  90. palkan_tula
    palkan
    What to choose?
    Bulma—admin dashboards, or you know some Vue
    Shoelace—CRUD, admin dashboards
    TailwindCSS—user-facing interfaces
    90

    View Slide

  91. In the end
    Or does it even matter?

    View Slide

  92. 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

    View Slide

  93. 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

    View Slide

  94. THANKS!
    @palkan
    @palkan_tula
    evilmartians.com
    @evilmartians
    discord.gg/stimulus-reflex

    View Slide