Slide 1

Slide 1 text

Frontendless Rails Frontend Vladimir Dementyev Evil Martians

Slide 2

Slide 2 text

palkan_tula palkan github.com/palkan 2

Slide 3

Slide 3 text

palkan_tula palkan Web development today 3 This is me 😎

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

palkan_tula palkan 6 Single engineer is enough to screw in a light bulb Full-stack is ... 🤔

Slide 7

Slide 7 text

palkan_tula palkan 7 Developing within a single ecosystem Full-stack is ... ☝

Slide 8

Slide 8 text

Once upon a time...

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

HTML (Haml/Slim) Helpers

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

The evolution of frontend

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

palkan_tula palkan 19 Living classics

Slide 20

Slide 20 text

palkan_tula palkan 20 Neoclassical

Slide 21

Slide 21 text

palkan_tula palkan hey.com 21

Slide 22

Slide 22 text

palkan_tula palkan NEW MAGIC hotwire.dev 22

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

palkan_tula palkan 24 ?

Slide 25

Slide 25 text

palkan_tula palkan 25 Client-side rendering Server-side rendering

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

palkan_tula palkan Example 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

palkan_tula palkan Example 31 <% 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 %>

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

palkan_tula palkan Stimulus 34 stimulusjs.org

Slide 35

Slide 35 text

palkan_tula palkan Example 35 Hide-able banners

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

palkan_tula palkan Example: Stimulus 37

AnyWork ...

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

palkan_tula palkan 40 Example Interactive forms with Stimulus and Vue

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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: '' } ); } disconnect() { if (this.vue) this.vue.$destroy(); } } Example

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Okay, okay, no more JavaScript!

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

palkan_tula palkan Phoenix LiveView 47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

palkan_tula palkan 50 Stimulus Reflex creator CableReady 🤔

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

palkan_tula palkan CableReady 55 cableready.stimulusreflex.com

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

palkan_tula palkan Example 58

Slide 59

Slide 59 text

palkan_tula palkan Example 59
data-reflex="change ->List#toggle_item_completion" data-item-id="<%= item.id %> > ...

<%= item.desc %>

...

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

palkan_tula palkan More HTML-over-WS Motion Turbo Streams 64

Slide 65

Slide 65 text

palkan_tula palkan Motion 65 github.com/unabridged/motion

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

palkan_tula palkan Turbo Streams 67
<%= 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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

palkan_tula palkan 71 Think in components

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

palkan_tula palkan Example 76 # app/components/button/component.html.erb <% if icon? %> <%= icon %> <% end %> <% == label %>

Slide 77

Slide 77 text

palkan_tula palkan 77 # some.html.erb
<%= render Button ::Component.new(label: "Like", icon: "❤") %>
Example

Slide 78

Slide 78 text

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
<%= render LikeButton.new %>
Example

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

palkan_tula palkan Preview Component 81

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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++

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

palkan_tula palkan Alternatives Cells hanami-view dry-view komponent elemental_components 86

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

palkan_tula palkan CSS after Bootstrap Bulma TailwindCSS Shoelace 88

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

palkan_tula palkan 90 Bulma + Stimulus + Buefy

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

palkan_tula palkan 92

Slide 93

Slide 93 text

palkan_tula palkan Web Components Customizable Accessibility 93

Slide 94

Slide 94 text

palkan_tula palkan 94 <=%=body%>

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

palkan_tula palkan Custom styles 🤔 96

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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; }

Slide 100

Slide 100 text

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

"> <%= body %>

Slide 101

Slide 101 text

palkan_tula palkan Example 101

Your notice has been created

Slide 102

Slide 102 text

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}`; }, }, } }

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

In the end Or does it even matter?

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

THANKS! @palkan @palkan_tula evilmartians.com @evilmartians