<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://thomascannon.me/">
  <id>https://thomascannon.me/</id>
  <title>Thomas Cannon's Blog</title>
  <updated>2025-04-06T00:00:00Z</updated>
  <link rel="alternate" href="https://thomascannon.me/" type="text/html"/>
  <link rel="self" href="https://thomascannon.me/feeds/posts.xml" type="application/atom+xml"/>
  <author>
    <name>Thomas Cannon</name>
    <uri>https://thomascannon.me</uri>
  </author>
  <entry>
    <id>tag:thomascannon.me,2025-04-06:/guides/the-rails-view-layer.html</id>
    <title type="html">Flip on the Awesome</title>
    <published>2025-04-06T00:00:00Z</published>
    <updated>2025-04-06T00:00:00Z</updated>
    <link rel="alternate" href="https://thomascannon.me/guides/the-rails-view-layer.html" type="text/html"/>
    <content type="html">
&lt;h1 id="flip-on-the-awesome"&gt;Flip on the Awesome&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Overhauling an app’s view layer without halting development&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’ve been facing a gnarly problem for the past year: what does a maintainable view layer look like in Rails? I’ve seen a different approach for every app I’ve worked on, and none of them have particularly fit the bill.&lt;/p&gt;

&lt;p&gt;For the 1&lt;sup&gt;st&lt;/sup&gt; version of the Practical Framework, I opted to dip my toes into Phlex 1. That approach served me well, allowing me to ship 2 apps in about a year. But then the tech debt bill came due from multiple angles.&lt;/p&gt;

&lt;p&gt;First: Web Awesome is nearing release. I’d hand-spun a frontend framework as an exercise in how to do so, and as a stopgap, but it frankly didn’t meet my quality standards long-term. So I knew I needed to replace it with Web Awesome when the time came.&lt;/p&gt;

&lt;p&gt;Second: Phlex 2.0 hit; which is a radical shift. I tried to upgrade what I had, realized it’s now much more opinionated and wants to handle the entire rendering stack. So I stepped back to square 1 to reevaluate the landscape.&lt;/p&gt;

&lt;p&gt;Third: practice reared its ugly head. Having built out a design system and frontend framework, gotten experience with multiple perspectives and approaches to the frontend, and shipped multiple products; I knew a lot more than I did in 2023. Plus, what’s widely available in browsers set the stage for some truly maintainable codebases.&lt;/p&gt;

&lt;p&gt;So, I spent months agonizing over different libraries, debated with folks about how to structure view code, and (most importantly) &lt;strong&gt;ported over an entire app&lt;/strong&gt; to figure out the best way to actually ship a view layer overhaul so that it doesn’t freeze an entire development team in its tracks.&lt;/p&gt;

&lt;p&gt;This guide tries to walk you through the things I learned, the approach I took, and why I’ve found a component-first, intentionally structured view layer backed by ViewComponent is the best way forward for Rails apps. And how to use feature flags and view variants to chip away at the project.&lt;/p&gt;

&lt;h2 id="how-to-read-this-guide"&gt;How to read this guide&lt;/h2&gt;

&lt;p&gt;This guide is &lt;strong&gt;long&lt;/strong&gt;, and covers a lot of ground. The structure is:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Discuss the big-picture plan and tools used, in general terms&lt;/li&gt;
  &lt;li&gt;Dig into some problem areas, principles, and tactics I applied&lt;/li&gt;
  &lt;li&gt;Some miscellaneous tips and bits of advice&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I recommend &lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot"&gt;opening up the repo&lt;/a&gt; to look at the code while reading through this guide. I’ll also include links to specific files when discussing particular topics. &lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot?tab=readme-ov-file#focus-areas"&gt;There is also this list of focus areas in the README.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After reading, I highly recommend forking the repo and playing around. You might not like how I built parts of it, you might want to refactor—this is a playground for you to test ideas and see how it works in a production app that’s just complex enough to really stretch your legs.&lt;/p&gt;

&lt;h3 id="what-is-dispatcher"&gt;What is Dispatcher?&lt;/h3&gt;

&lt;p&gt;It is an app I was commissioned to write, which I use as testbed. I’ve opened sourced a snapshot of the app right when I turned on the alpha for the Web Awesome UI, to act as a public resource for folks to study.&lt;/p&gt;

&lt;p&gt;It’s a simple job and onsite management tool for service companies. The office specifies a job, the tasks it requires, sets up onsites, and assigns tasks to those onsites. Onsites can have notes attached to them. Your standard states &amp;amp; priority flags apply as well.&lt;/p&gt;

&lt;h4 id="what-is-practicalframework-what-is-practicalviews"&gt;What is &lt;code&gt;PracticalFramework&lt;/code&gt;? What is &lt;code&gt;PracticalViews&lt;/code&gt;?&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;PracticalFramework&lt;/code&gt; is the layer I’m building on top of Rails to be used across a suite of apps. It’s meant to distill and standardize solutions to common problems across many apps, and will eventually be open-sourced. The versions in these snapshots are pre-releases, not documented and or refined for public use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PracticalViews&lt;/code&gt; is the namespace I’m using for this 2&lt;sup&gt;nd&lt;/sup&gt; iteration of the framework’s view layer. I needed a wholly separate namespace to avoid collisions, and I was frankly tired of having to write &lt;code&gt;PracticalFramework::Components::&lt;/code&gt; before everything.&lt;/p&gt;

&lt;h1 id="the-big-picture"&gt;The Big Picture&lt;/h1&gt;

&lt;h2 id="tools-used"&gt;Tools used&lt;/h2&gt;

&lt;h3 id="flipper"&gt;Flipper&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.flippercloud.io"&gt;Flipper&lt;/a&gt; is the feature flag library I use. This gives you the distinct advantage of not having a long-running branch for the overhaul, with all the nightmare merge commits and rebasing. You tackle it piecemeal, just like you would a Rails upgrade with the dual-boot method.&lt;/p&gt;

&lt;p&gt;You can use whatever feature flag approach that you want; it doesn’t matter. I use Flipper because it’s simple, straightforward, and Just Works™.&lt;/p&gt;

&lt;h3 id="viewcomponent"&gt;ViewComponent&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://viewcomponent.org"&gt;ViewComponent&lt;/a&gt; is a library for building components in Ruby on Rails. Initially developed at GitHub; it is mature, fairly flexible, and supports a wide variety of beliefs around componentization—from everything being a component, to lightweight wrappers around handcrafted markup.&lt;/p&gt;

&lt;h3 id="web-awesome"&gt;Web Awesome&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://backers.webawesome.com"&gt;Web Awesome&lt;/a&gt; is the frontend component library Practical Computer will be using going forward.&lt;/p&gt;

&lt;h3 id="lookbook"&gt;Lookbook&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://lookbook.build"&gt;Lookbook&lt;/a&gt; is A UI preview tool for components in Rails apps, akin to Storybook. It supports ViewComponent out of the box, and allows you to quickly iterate &amp;amp; review your component library.&lt;/p&gt;

&lt;h3 id="oaken"&gt;Oaken&lt;/h3&gt;

&lt;p&gt;Although I didn’t get around to using &lt;a href="https://github.com/kaspth/oaken/tree/main"&gt;Oaken&lt;/a&gt; to build the seeds for my Lookbook previews, it’s an invaluable tool I install on every project I can.&lt;/p&gt;

&lt;h2 id="the-phases-of-a-view-layer-overhaul"&gt;The phases of a View Layer overhaul&lt;/h2&gt;

&lt;figure&gt;
  &lt;img src="/assets/view-layer/PhasesofaViewLayerOverhaul.png" alt="A diagram of how incremental PRs, going from experimental to structured, allow you to replace an exisitng UI with a new one without halting development" /&gt;
  &lt;figcaption&gt;
  &lt;p&gt;With incremental PRs and feature flags, the daunting task of a UI overhaul becomes manageable&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;ol&gt;
  &lt;li&gt;Prepare:
    &lt;ul&gt;
      &lt;li&gt;Decide on big-picture conventions, front-end frameworks, etc.&lt;/li&gt;
      &lt;li&gt;Identify pain points in your existing view layer that you want to eliminate (eventually!)&lt;/li&gt;
      &lt;li&gt;Ensure your controller layer is at the quality level you can live with for both UIs. &lt;em&gt;Technically,&lt;/em&gt; you can defer this for problem areas until you port that area over, or kick the can to post-overhaul, but generally speaking, you’re going to be living with your existing controller layer for &lt;strong&gt;a while&lt;/strong&gt;.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Sketch new view layer foundation
    &lt;ul&gt;
      &lt;li&gt;Build the “kernel” of the new view layer, using View Variants &amp;amp; separated asset builds.&lt;/li&gt;
      &lt;li&gt;Create a few of your foundational ViewComponents.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Port existing UI to components.
    &lt;ul&gt;
      &lt;li&gt;A slog, broken up into smaller PRs to keep the project manageable.&lt;/li&gt;
      &lt;li&gt;Focus &lt;strong&gt;solely&lt;/strong&gt; on porting the existing behavior; do not get sidetracked into improving existing behavior (aside from 5 minute treats).&lt;/li&gt;
      &lt;li&gt;Only tackle the view layer, your requests should remain the same between both versions of the UI. Put another way: everything from the controller layer up should not change.&lt;/li&gt;
      &lt;li&gt;Work at a steady pace, with intention, to keep momentum and refine lessons as the problem space is fresh.&lt;/li&gt;
      &lt;li&gt;Don’t worry about tests or previews at this stage, you have bigger problems to tackle (like the scale of porting).&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Once everything is ported, &lt;em&gt;then&lt;/em&gt; get the test suite green against the new UI.
    &lt;ul&gt;
      &lt;li&gt;Patch tests based on which version of the UI is running.&lt;/li&gt;
      &lt;li&gt;Keep patches focused on minimal changes, gated behind feature flags.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Now that everything is ported over to components, do an internal polishing pass.
    &lt;ul&gt;
      &lt;li&gt;This is when tests and previews get setup.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Gradual rollout to customers
    &lt;ul&gt;
      &lt;li&gt;Initial alpha release, then percentage-based gradual releases.&lt;/li&gt;
      &lt;li&gt;Fix bugs as they crop up, you get to choose the impact of bugs by the sample size of customers using the new UI.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Once the rollout is complete, cleanup and remove the old UI.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id="how-to-overhaul-your-view-layer-while-continuing-to-ship-features"&gt;How to overhaul your view layer while continuing to ship features&lt;/h2&gt;

&lt;p&gt;In short: Feature Flags! You’re going to be building a whole new version of the UI, gated behind a feature flag. This will let you keep merging code back into &lt;code&gt;main&lt;/code&gt;, tackle other important projects, and chip away at the UI with minimal overhead.&lt;/p&gt;

&lt;h3 id="keep-the-old--new-view-code-totally-separate-with-view-variants-and-different-asset-builds"&gt;Keep the old &amp;amp; new view code totally separate with view variants and different asset builds&lt;/h3&gt;

&lt;p&gt;To reduce bugs and avoid carrying over unused code to the new pastures; you should use &lt;a href="https://guides.rubyonrails.org/layouts_and_rendering.html#the-variants-option"&gt;Rails’ request variants to build your new views&lt;/a&gt;. This allows you to build an entirely new view stack in parallel to your existing one, keeping your existing view code untouched. You can experiment away, take radical swings, find the best path forward without breaking the code that keeps your business running.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;ApplicationController&lt;/code&gt;, I added a &lt;code&gt;before_action&lt;/code&gt; to switch the variant based on whether we were rendering the Web Awesome UI:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;using_web_awesome?&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_current_organization?&lt;/span&gt;
    &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:web_awesome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:web_awesome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FlipperNoOrganizationActor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_webawesome_variant&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;using_web_awesome?&lt;/span&gt;
  &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:webawesome&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/controllers/application_controller.rb#L70-L82"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/controllers/application_controller.rb#L70-L82&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With that, I could tackle each controller view one at a time; and ended up with directories that looked like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;app/views/organizations/jobs/_datatable_form.html.erb
app/views/organizations/jobs/_form.html.erb
app/views/organizations/jobs/_job_table.html.erb
app/views/organizations/jobs/_job_table_row.html.erb
app/views/organizations/jobs/edit.html+webawesome.erb # New Web Awesome Version
app/views/organizations/jobs/edit.html.erb
app/views/organizations/jobs/index.html+webawesome.erb # New Web Awesome Version
app/views/organizations/jobs/index.html.erb
app/views/organizations/jobs/new.html+webawesome.erb # New Web Awesome Version
app/views/organizations/jobs/new.html.erb
app/views/organizations/jobs/show.html+webawesome.erb # New Web Awesome Version
app/views/organizations/jobs/show.html.erb
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It’s a little messy, but it’s baked into the framework and widely supported.&lt;/p&gt;

&lt;p&gt;Make sure to also check out &lt;code&gt;app/layouts&lt;/code&gt;, to see how the entire view stack is fresh ground because of view variants.&lt;/p&gt;

&lt;p&gt;For the Javascript &amp;amp; CSS, I created entirely new entrypoints, prefixed with &lt;code&gt;v2-*&lt;/code&gt; to help keep things separated.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;app/javascript/application.js
app/javascript/boot.js
...
app/javascript/chunks/kernel.js
app/javascript/chunks/v2-kernel.js
app/javascript/elements/index.js
app/javascript/elements/google-place-autocomplete.js
app/javascript/elements/v2-index.js
app/javascript/elements/v2-google-place-autocomplete.js
...
app/javascript/theme-toggle.js
app/javascript/v2-application.js
app/javascript/v2-boot.js
## ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;There is technically duplicated code in this structure, but it’s a tradeoff for keeping things siloed to avoid bugs and make cleanup post-migration easier. Depending on your needs &amp;amp; codebase, it is possible to refactor shared code into utility files to reduce duplication.&lt;/p&gt;

&lt;h2 id="why-viewcomponent"&gt;Why ViewComponent?&lt;/h2&gt;

&lt;p&gt;A lot of time was spent debating between Phlex or ViewComponent. Especially because Dispatcher was built with a lot of Phlex 1 components, and I like Phlex’s ergonomics!&lt;/p&gt;

&lt;p&gt;There are a few factors that led to ViewComponent winning out over Phlex 2+:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Phlex 2+’s strongly encouraged design ethos that the entire view stack to be Phlex-based&lt;/li&gt;
  &lt;li&gt;The knock-on effects of Phlex 2+’s push for structured document rendering, versus Rails’ buffer-based rendering (which ViewComponent extends)&lt;/li&gt;
  &lt;li&gt;ViewComponent accomplishing almost everything that I needed for my view layer; once I enabled the experimental capture patch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I actually did try migrating to Phlex 2; which was a massive undertaking. But right at the end of migrating Dispatcher, I ran into some &lt;a href="https://github.com/phlex-ruby/phlex-rails/issues/282https://github.com/phlex-ruby/phlex-rails/issues/282"&gt;breaking issues with form builders&lt;/a&gt;, which I use extensively.&lt;/p&gt;

&lt;h3 id="the-experimental-capture-patch"&gt;The “Experimental” capture patch&lt;/h3&gt;

&lt;p&gt;I’d initially written off ViewComponent due to &lt;a href="https://viewcomponent.org/known_issues.html#compatibility-with-rails-form-helpers"&gt;this section of the Known Issues&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;ViewComponent isn’t compatible with &lt;code&gt;form_for&lt;/code&gt; helpers by default.&lt;/p&gt;

  &lt;p&gt;Passing a form object (often &lt;code&gt;f&lt;/code&gt;) to a ViewComponent works for simple cases like &lt;code&gt;f.text_field :name&lt;/code&gt;. Content may be ill-ordered or duplicated in complex cases, such as passing blocks to form helpers or when nesting components.&lt;/p&gt;

  &lt;p&gt;Some workarounds include:&lt;/p&gt;

  &lt;p&gt;Experimental: Enable the capture compatibility patch with &lt;code&gt;config.view_component.capture_compatibility_patch_enabled = true&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first glance, a non-starter. But, after taking Kasper Timm Hansen’s &lt;a href="https://lu.ma/user/kaspth"&gt;Deep Dive into ActionView&lt;/a&gt;, I finally understood how it composes buffers and the &lt;a href="https://guides.rubyonrails.org/layouts_and_rendering.html#rendering-objects"&gt;way ViewComponent hooks into ActionView&lt;/a&gt;. That, plus some more detective work, led me to find out that the capture patch was merged on &lt;a href="https://github.com/ViewComponent/view_component/pull/1650"&gt;February 2023&lt;/a&gt;. So it’s had 2+ years of incubation; worth experimenting! And it turned out to work just as expected!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/lib/view_component/capture_compatibility_patch.rb"&gt;I &lt;em&gt;did&lt;/em&gt; have to patch the patch, but that seems to only be due to having existing Phlex 1 components&lt;/a&gt;. The patch itself is tiny: just check that the &lt;code&gt;block_context&lt;/code&gt; has a &lt;code&gt;class&lt;/code&gt; before trying to check if it’s a subclass.&lt;/p&gt;

&lt;h2 id="why-web-awesome"&gt;Why Web Awesome?&lt;/h2&gt;

&lt;p&gt;Having researched, debated, and chewed on the problem of component libraries, their dependence on Javascript, and the people behind them; Web Awesome has come out as the winner. The team behind it believes in the foundations of the web, they’ve shipped native styles that mirror their components so you don’t need to rely on Javascript for your app to be functional, and have thought through the problems of maintenance and customizability.&lt;/p&gt;

&lt;p&gt;Even in its alpha state, it’s been a joy to work in and has generally worked with only minor issues.&lt;/p&gt;

&lt;p&gt;I’m not particularly worried about having to redo all of my work once the full version is released. Because the new view layer is component-based, applying fixes is a matter of changing the components and they will propagate across the entire app. That, and the time investment that comes with porting, it was better to get started on the migration now rather than wait for 1.0.&lt;/p&gt;

&lt;h1 id="problem-areas-principles-and-tactics"&gt;Problem Areas, Principles, and Tactics&lt;/h1&gt;

&lt;h2 id="avoiding-the-pitfalls-of-a-ui-overhaul"&gt;Avoiding the pitfalls of a UI overhaul&lt;/h2&gt;

&lt;p&gt;UI overhauls are dangerous, daunting tasks. They shouldn’t be taken lightly, and they’re a long-term investment. Unfortunately, they’re also necessary!&lt;/p&gt;

&lt;p&gt;To keep the project on track, you need to keep the following in mind:&lt;/p&gt;

&lt;h3 id="the-scope-needs-to-be-kept-strictly-in-check"&gt;The scope needs to be kept strictly in check&lt;/h3&gt;

&lt;p&gt;For the migration, only focus on the view layer. As much as possible, do not touch from the controller layer upwards. Otherwise, the scope of the project will spiral out of control. It’ll also become a maintenance nightmare, as you juggle the behavior of 2 different UI implementations. It’s much better to accept the current state of your backend while you overhaul your view layer.&lt;/p&gt;

&lt;p&gt;If you &lt;strong&gt;do&lt;/strong&gt; need to tackle stuff in the controller layer, that should be done with feature flags An example of this is rendering flash messages. In Dispatcher, I’ve taken the approach of having rich payloads for my flash messages, which are then serialized at render-time. Since there is an entirely new view layer, I needed to patch the original flash helpers from &lt;code&gt;PracticalFramework&lt;/code&gt; to return icons from the &lt;code&gt;IconSet&lt;/code&gt; if &lt;code&gt;using_web_awesome?&lt;/code&gt;&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;## app/controllers/concerns/patched_flash_helpers.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;PatchedFlashHelpers&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;default_notice_icon&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;using_web_awesome?&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info_icon&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;style: &lt;/span&gt;&lt;span class="s1"&gt;'fa-duotone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'circle-info'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;default_alert_icon&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;using_web_awesome?&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alert_icon&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;style: &lt;/span&gt;&lt;span class="s1"&gt;'fa-duotone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'triangle-exclamation'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;default_success_icon&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;using_web_awesome?&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success_icon&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;style: &lt;/span&gt;&lt;span class="s1"&gt;'fa-duotone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'circle-check'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Relevant files:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/vendor/gems/practical-framework-0.2.5/lib/practical_framework/controllers/flash_helpers.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/vendor/gems/practical-framework-0.2.5/lib/practical_framework/controllers/flash_helpers.rb&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/controllers/organizations/jobs_controller.rb#L90-L119"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/controllers/organizations/jobs_controller.rb#L90-L119&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/controllers/concerns/patched_flash_helpers.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/controllers/concerns/patched_flash_helpers.rb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="refinements-will-happen-over-time"&gt;Refinements will happen over time&lt;/h3&gt;

&lt;p&gt;This is why it’s important to use feature flags and small PRs, because as you build out the new UI using components, you’ll change your approach. Something that seemed good in isolation might need reworked. A component’s shape will change. Something that &lt;em&gt;was&lt;/em&gt; a component will end up as a helper method.&lt;/p&gt;

&lt;p&gt;I think view componentizing your UI for the first time is akin to your first moves from spaghetti code to OOP. You stare into the void, and it’s staring back into you. You’re going to get things wrong and the process is going to change you a little bit. That’s scary, but good things can come from it.&lt;/p&gt;

&lt;p&gt;You’re also not going to be as productive as you think you’ll be, &lt;em&gt;because&lt;/em&gt; you’re actively growing and changing your approaches. So while you think you might be going from A to B while migrating, you’re actually going from 1 to 5. And you need to through 2, 3, and 4 to get there properly.&lt;/p&gt;

&lt;h2 id="why-do-seasoned-developers-balk-at-componentization"&gt;Why do seasoned developers balk at componentization?&lt;/h2&gt;

&lt;p&gt;A quirk I’ve consistently run into with Rails developers (even highly experienced ones!) is resistance towards a component structure for view code.&lt;/p&gt;

&lt;p&gt;My theory is that it’s a combination of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Lack of good examples of non-trivial, non-painfully generic components that are difficult to maintain.&lt;/li&gt;
  &lt;li&gt;Lack of techniques on how to approach the problem at scale with existing codebases.&lt;/li&gt;
  &lt;li&gt;Visceral pushback against reactive frameworks and the complexities they introduce.&lt;/li&gt;
  &lt;li&gt;An outdated understanding of what the browser brings to the table.&lt;/li&gt;
  &lt;li&gt;Pushing back on the overcorrection of “components all the way!”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="lack-of-examples--techniques"&gt;Lack of examples &amp;amp; techniques&lt;/h3&gt;

&lt;p&gt;I’m trying to address the lack of examples and techniques with this guide, and by publishing that snapshot of a production repo. It is deliberately not meant to be the ideal end-state. It’s a snapshot of Dispatcher when I finished the most arduous task of the migrating: porting the view layer so that there is a component-first version.&lt;/p&gt;

&lt;p&gt;There are bits of the code I’m unhappy with, improvements to clean up early prototypes, the new design’s markup is definitely WIP. But all of those are known, solvable problems you either have experience with, or can find other resources for. As far as I know, there aren’t many examples of this kind of production-grade snapshot freely available to tinker with and analyze.&lt;/p&gt;

&lt;h3 id="reactive-frameworks-poisoned-the-well-on-great-ergonomics"&gt;Reactive frameworks poisoned the well on great ergonomics&lt;/h3&gt;

&lt;p&gt;Let me show a quick Swift UI example (&lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/building-a-menu-using-list"&gt;pulled from Hacking With Swift&lt;/a&gt;):&lt;/p&gt;

&lt;div class="language-swift highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;ContentView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;menu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="kt"&gt;MenuSection&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"menu.json"&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;View&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;NavigationView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;List&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;menu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
          &lt;span class="kt"&gt;Section&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;ForEach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
              &lt;span class="kt"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigationTitle&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Menu"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;GroupedListStyle&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;figure&gt;
  &lt;img src="/assets/view-layer/2-6.png" alt="A screenshot of the code above in an Xcode window, showing the preview of a list navigable list of items in sections" /&gt;
  &lt;figcaption&gt;
  &lt;p&gt;&lt;a href="https://www.hackingwithswift.com/quick-start/swiftui/building-a-menu-using-list"&gt;From Hacking with Swift&lt;/a&gt;&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Don’t worry about the specifics too much, but keep the general feeling in mind. The reason I mention Swift UI specifically is that the promise behind its intention is:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;You write what is essentially markup, and let the platform determine the implementation details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think the React/Reactive UI paradigm has poisoned the well in people’s mind on these ergonomics. The problem is that this approach is commonly associated with reactive UIs, which are just too gnarly for most apps.&lt;/p&gt;

&lt;p&gt;But remove the reactive UI aspect from your mind, and focus on this example just as a way to render a view that has a traditional lifecycle. This approach to view rendering can act as a force-multiplier for building and maintaining complex apps. It allows you to set up your intentions for how an app behaves, then apply those intentions in various contexts. Implementation details are propagated throughout the app with small PRs, and the behavior is consistent because the code you wrote is based on the &lt;em&gt;intention&lt;/em&gt;, not the presentation. Especially if you take care when building out your components at the atomic level, then provide good intentional abstractions. A good modal dialog component will ensure that every modal dialog in your app behaves the same way, but doesn’t &lt;em&gt;get in the way&lt;/em&gt; of a specific part of the application.&lt;/p&gt;

&lt;p&gt;So, the gap here is providing an abstraction for the view rendering. At the application level, if I’m running a suite of apps (or a single complex app), I (ideally) don’t want to care about the specific markup for a dropdown menu in 90% of cases. I’ve already made all those choices beforehand. And when I fix bugs, I want to be able to apply those fixes across the board. It’s a foundational principle of OOP.&lt;/p&gt;

&lt;p&gt;Critics argue this is overkill for most applications; I argue it’s simply a different set of skills that have been historically downplayed (&lt;a href="https://thoughtbot.com/blog/tailwind-and-the-femininity-of-css"&gt;like the skill set required to write good HTML &amp;amp; CSS&lt;/a&gt;). You need to understand cascading, composition, the balance of flexibility &amp;amp; intention.&lt;/p&gt;

&lt;h3 id="the-browser-is-bringing-a-lot-more-to-the-table"&gt;The browser is bringing a lot more to the table&lt;/h3&gt;

&lt;p&gt;Before the prevalence of componentized HTML frontends, the view layer for Rails apps could best be described as “spaghetti, refined.” Every view is (by and large) bespoke, with &lt;em&gt;some&lt;/em&gt; logic shared between them.&lt;/p&gt;

&lt;p&gt;Part of this is a framework problem, Rails has always punted on maintainable view code and scoffed at the criticisms that what it ships is &lt;em&gt;not&lt;/em&gt; maintainable. &lt;a href="https://thomascannon.me/guides/fixing-the-rails-networking-stack"&gt;I’ve already written 3,000+ words about Rails’ networking stack, so &lt;em&gt;moving on&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Another part is that the landscape &amp;amp; tooling in browsers was &lt;em&gt;very&lt;/em&gt; fresh and in flux until the 2020s.&lt;/p&gt;

&lt;p&gt;But browsers have made significant advancements in enabling componentized frontend code. To name a few:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CSS Layers&lt;/li&gt;
  &lt;li&gt;Custom elements&lt;/li&gt;
  &lt;li&gt;ESM support&lt;/li&gt;
  &lt;li&gt;The move away from callbacks to &lt;code&gt;async&lt;/code&gt; code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these features allow you to break up your code into layers that &lt;em&gt;compose on top of&lt;/em&gt; each other, instead of being tightly coupled.&lt;/p&gt;

&lt;h3 id="components-arent-a-panacea"&gt;Components aren’t a panacea&lt;/h3&gt;

&lt;p&gt;There’s justifiable fear and uncertainty about adopting components and whether it’s a good idea for your app. It’s a daunting paradigm shift that seasoned developers don’t usually face. And if they have faced it, it’s generally been intrinsically tied with reactive UIs, Hail Mary product rescues (that often fail), or dysfunctional product management.&lt;/p&gt;

&lt;p&gt;People only look at the view layer once it’s too late; so they overcorrect and try to go completely rigid with full-on componentization. It’s one of the clearest places where the growing pains of an application over time become apparent. Models, controllers, services, etc. get refactored and cleaned up constantly because they’re high-traction. It’s rare that a part of the view layer gets revisited &lt;em&gt;unless&lt;/em&gt; new features are added or a major performance issue stems from the view layer.&lt;/p&gt;

&lt;p&gt;You can’t simply go all-in on components and expect everything to be hunk-dory and perfect. Poorly applied, it leads to a ton of indirection and rigidity. You lose the fun of prototyping. A bad framework can cripple your app. This is a deliberate decision you have to make, knowing the tradeoffs that are going to come from it in terms of the risk of overhead/indirection. &lt;strong&gt;But&lt;/strong&gt;, like all good frameworks; there is the possibility of it significantly improving maintainability. It requires good stewardship, and respect for the craft.&lt;/p&gt;

&lt;p&gt;When you get to the point where you’re overhauling your UI, you get the chance to adopt this new landscape, change your approach, and learn from your mistakes. Adopting ViewComponents is &lt;strong&gt;not&lt;/strong&gt; about making everything rigid beyond flexibility. What are the patterns, details, and rules you want to lock-in to components so that you are free to tackle other problems? When do you let something live freeform, and when do you realize it needs to be encapsulated in a component?&lt;/p&gt;

&lt;p&gt;Good view code is intentionally revealing, and components are a tool to help structure your intentions. A good view layer allows for fluidity, experimentation, and (some) messiness. There’s a reason why sheet music is &lt;em&gt;not&lt;/em&gt; strictly followed. Part of a song’s performance is up to the interpretation of the players. Carefully adopting ViewComponents involves choosing &lt;em&gt;where&lt;/em&gt; to be rigid.&lt;/p&gt;

&lt;h2 id="componentization-beyond-toy-examples-and-tutorial-blogs"&gt;Componentization beyond toy examples and tutorial blogs&lt;/h2&gt;

&lt;p&gt;Another problem that proponents of componentization run into is &lt;strong&gt;the lack of good, real-world examples to point to&lt;/strong&gt;. Articles &amp;amp; tutorials about component libraries and using ViewComponents have examples that are far too rudimentary to convince a seasoned developer. They’re right to be skeptical! Show them the evidence!&lt;/p&gt;

&lt;p&gt;Again, why I published that snapshot of Dispatcher! It’s not perfect, but I’d argue it’s pretty good. And it’s actual production code of an alpha state.&lt;/p&gt;

&lt;p&gt;I want to dig into two specific examples from Dispatcher, to highlight the benefits of components to solve two specific problems that would be a nightmare to tackle in spaghetti views:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Polishing an app-wide UI component&lt;/li&gt;
  &lt;li&gt;Replacing old code with native browser features&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="polishing-an-app-wide-ui-component"&gt;Polishing an app-wide UI component&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Improving the markup around inputs and field titles&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Generally, form inputs have a similar structure:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The label for the field&lt;/li&gt;
  &lt;li&gt;The input itself&lt;/li&gt;
  &lt;li&gt;The error messages for this field&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Dispatcher, this is broken up into 2 components:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;PracticalViews::Form::FieldTitleComponent&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;PracticalViews::Form::InputComponent&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the actual form code, these are extracted into helper methods (we’ll go over this later in the guide):&lt;/p&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;## app/views/components/forms/job_form_component.html.erb
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_label&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;job_name_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_field&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatcher.placeholders.job_name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;autofocus: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;autocomplete: &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"focusout-validation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When I was porting over everything to Web Awesome, using these components, I did not have to care about polishing. I could focus on keeping the momentum for porting, because &lt;strong&gt;maintainable polishing is baked into the nature of components&lt;/strong&gt;. At any point in the future, I can clean up how either of these components render, agonizing over the spacing, typography, structure, accessibility; and the changes will be propagated app-wide.&lt;strong&gt;What would be a 3,000 line copy/paste PR is reduced to a 30-line, 10 minute code review.&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id="replacing-old-code-with-native-browser-features"&gt;Replacing old code with native browser features&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Changing “open dialog” buttons to use the Invoker Commands API if it becomes Baseline&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For those who haven’t checked out the Invoker Commands API:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The Invoker Commands API provides a way to declaratively assign behaviors to buttons, allowing control of interactive elements when the button is enacted (clicked or invoked via a keypress, such as the spacebar or return key).&lt;/p&gt;

  &lt;p&gt;[…]&lt;/p&gt;

  &lt;p&gt;Historically creating these kinds of controls has required JavaScript event listeners added to the button which can then call the APIs on the element they control. The &lt;code&gt;commandForElement&lt;/code&gt; and &lt;code&gt;command&lt;/code&gt; properties provide a way to do this declaratively for a limited set of actions. This can be advantageous for built-in commands as the user does not have to wait for JavaScript to download and execute to make these buttons interactive.  [&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API"&gt;MDN&lt;/a&gt;]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is great; I hope it ships and becomes a Baseline feature! And when it does, I can easily upgrade my entire app to use the API with a tiny PR.&lt;/p&gt;

&lt;p&gt;All modal dialogs in Dispatcher use the &lt;code&gt;PracticalViews::OpenDialogButtonComponent&lt;/code&gt;. This component currently writes inline Javascript on the &lt;code&gt;onclick&lt;/code&gt; handler to open a dialog based on its ID:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;## app/views/components/practical_views/open_dialog_button_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PracticalViews::OpenDialogButtonComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ButtonComponent&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:dialog_id&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dialog_id&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;appearance: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;color_variant: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;size: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_defaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;onclick: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inline_js_to_open_dialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;dialog_id: &lt;/span&gt;&lt;span class="n"&gt;dialog_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;type: :button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;appearance: &lt;/span&gt;&lt;span class="n"&gt;appearance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;color_variant: &lt;/span&gt;&lt;span class="n"&gt;color_variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;size: &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inline_js_to_open_dialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dialog_id&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"document.getElementById(`&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dialog_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`).showModal()"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/open_dialog_button_component.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/open_dialog_button_component.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s not perfect, but it’s the most error-resistant way to ensure dialog buttons always work, regardless of the state of the rest of the application’s JS.&lt;/p&gt;

&lt;p&gt;When the Invoker Commands API becomes Widely available, I can change this component (and the &lt;code&gt;PracticalViews::ModalDialogComponent&lt;/code&gt;), and again, the changes will be propagated app-wide.&lt;/p&gt;

&lt;p&gt;These are not toy examples, they are foundational UI components that every app needs. These two components are basically in every single view for the app, in one way or another. They dictate foundational experiences people have using your software. Taking a component-first approach changes maintenance tasks that would constantly be pushed back because the benefits outweigh the effort required into lazy Friday PRs.&lt;/p&gt;

&lt;h3 id="learning-more-about-componentization"&gt;Learning more about componentization&lt;/h3&gt;

&lt;p&gt;Even though I strongly disagree with the use of Tailwind, Jeremy Smith’s talk “&lt;a href="https://hybrd.co/talks"&gt;Refactoring Volatile Views into Cohesive Components&lt;/a&gt;” has some great breakdowns in how views get complex, and ways that components can help mitigate that complexity.&lt;/p&gt;

&lt;h2 id="decouple-formbuilders-with-sidechained-components"&gt;Decouple &lt;code&gt;FormBuilders&lt;/code&gt; with sidechained components&lt;/h2&gt;

&lt;p&gt;I know &lt;code&gt;FormBuilders&lt;/code&gt; have a bad reputation and are discouraged by some because they’re too tightly coupled with the ActiveModel layer.&lt;/p&gt;

&lt;p&gt;From my experience, &lt;code&gt;FormBuilder&lt;/code&gt; encapsulates two different features:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Mapping a model (virtual or otherwise) to properties that are needed for HTML forms&lt;/li&gt;
  &lt;li&gt;Rendering the markup necessary for HTML forms&lt;/li&gt;
&lt;/ol&gt;

&lt;figure&gt;
  &lt;img src="/assets/view-layer/FormBuildersbydefault.png" alt="A diagram of Rails' FormBuilders encapsulate 2 different problems, with the markup rendering being the least scalable" /&gt;
  &lt;figcaption&gt;
  &lt;p&gt;&lt;code&gt;FormBuilders&lt;/code&gt; markup rendering is too annyoing to extend out of the box.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The first feature is an extreme time saver, and shouldn’t be discarded. Not having to think about parameter name/value serialization for forms, accessing errors for particular fields, and having a generalized interface for accessing the underlying model are all invaluable time savers.&lt;/p&gt;

&lt;p&gt;The second feature is…lacking for any complex application. Mucking around with tag concatenation &amp;amp; generating markup from within a general-purpose class that &lt;em&gt;also&lt;/em&gt; intermingles methods for those time saver features above is a recipe for brittle code that only a few people can maintain.&lt;/p&gt;

&lt;p&gt;It also doesn’t help that FormBuilders are extremely undocumented, I’d never fully realized their utility &amp;amp; purpose until 2023. And that one of the largest extensions of &lt;code&gt;FormBuilders&lt;/code&gt; is Simple Form, the bane of my existence.&lt;/p&gt;

&lt;p&gt;Let’s take a detour to talk about &lt;em&gt;why&lt;/em&gt; Simple Form is bad because it highlights the problems of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Overgeneralization&lt;/li&gt;
  &lt;li&gt;Bad API design&lt;/li&gt;
  &lt;li&gt;Inflexibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="overgeneralization--inflexibility"&gt;Overgeneralization &amp;amp; Inflexibility&lt;/h3&gt;

&lt;p&gt;Simple Form shoves everything into an &lt;code&gt;input&lt;/code&gt; method, which doesn’t account for the fact that UIs cannot be fully standardized. Not every field should be rendered the same way; heck, not every &lt;em&gt;text&lt;/em&gt; field is rendered the same way. That means that your forms are all funneled into the same markup flow, regardless of whether it actually makes anything sense for that particular UI element.&lt;/p&gt;

&lt;p&gt;And because Simple Form is designed to overtake your entire form rendering pipeline, it’s not easy to make modifications. You can’t sidestep Simple Form to say&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Listen, I know this form is going to be different than the others you’ve seen before. That’s okay. Don’t panic. I’m a professional, I’ve got this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id="bad-api-design"&gt;Bad API Design&lt;/h3&gt;

&lt;p&gt;Simple Form &lt;em&gt;does&lt;/em&gt; have a way to extend or customize inputs. The problem is that it’s one of the single worst developer experiences I’ve ever had to deal with. Everything is handled through a configuration block, requiring restarts to see any changes. There isn’t a way to just…specify the markup for a particular component, you have to wrap everything in tags. If you want to do stuff like add a wrapper element, that’s all handled with nested &lt;code&gt;options:&lt;/code&gt; hashes that are a roll of the dice.&lt;/p&gt;

&lt;p&gt;By the time you’d figure out how to make Simple Form work, you’re too exhausted and frustrated to actually &lt;em&gt;care&lt;/em&gt; about the user experience. So you slap together something and let the customer deal with the remaining pain.&lt;/p&gt;

&lt;h3 id="sidechained-viewcomponents"&gt;Sidechained &lt;code&gt;ViewComponents&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;However, a clever application of &lt;code&gt;ViewComponents&lt;/code&gt; sidesteps this problem. By creating a series of components that take in a &lt;code&gt;FormBuilder&lt;/code&gt; instance, you can sidechain the rendering back to the View Layer. This keeps as much of the complexity of rendering markup in the View Layer (where it belongs) and &lt;strong&gt;out&lt;/strong&gt; of the &lt;code&gt;FormBuilder&lt;/code&gt;. And with good API design, you can keep your underlying &lt;code&gt;FormBuilder&lt;/code&gt; close to its vanilla state to handle the edge cases &amp;amp; wholly bespoke UI.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src="/assets/view-layer/FormBuildersside-loading.png" alt="A diagram of using ViewComponents with Rails' FormBuilders by having lightweight, optional helper methods inside the form builders that call ViewComponents." /&gt;
  &lt;figcaption&gt;
  &lt;p&gt;By sidechaining the rendering of markup into optional ViewComponents, &lt;code&gt;FormBuilders&lt;/code&gt; are significantly more maintainable and useful.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id="example-forminputcomponent"&gt;Example: &lt;code&gt;Form::InputComponent&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Let’s look at a common UI component that Simple Form purports to solve: Wrapping a field with a label, and the errors for that specific field.&lt;/p&gt;

&lt;p&gt;This is how the component is used in the actual form:&lt;/p&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;# app/views/components/forms/job_form_component.html.erb
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_label&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;job_name_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_field&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatcher.placeholders.job_name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;autofocus: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;autocomplete: &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"focusout-validation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;All the forms in the new View Layer use a lightweight subclass of &lt;code&gt;ActionView::Helpers::FormBuilder&lt;/code&gt;. This is where the &lt;code&gt;input_component&lt;/code&gt; helper method is defined:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;## app/lib/practical_views/form_builders/web_awesome.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PracticalViews::FormBuilders::WebAwesome&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Helpers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FormBuilder&lt;/span&gt;
  &lt;span class="c1"&gt;# …&lt;/span&gt;

  &lt;span class="c1"&gt;# also shown because it is used by the example above&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;field_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Form&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FieldTitleComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_icon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="n"&gt;title&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;input_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label_options: &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Form&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InputComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;f: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;object_method: &lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;label_options: &lt;/span&gt;&lt;span class="n"&gt;label_options&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/lib/practical_views/form_builders/web_awesome.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/lib/practical_views/form_builders/web_awesome.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This helper method is deliberately lightweight: it’s meant to DRY up using this &lt;code&gt;ViewComponent&lt;/code&gt;, keeping the end-result forms less boilerplate, while not caring at all about the end-result markup.&lt;/p&gt;

&lt;p&gt;This is what the &lt;code&gt;Form::InputComponent&lt;/code&gt; looks like:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;## app/views/components/practical_views/form/input_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PracticalViews::Form::InputComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:object_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:label_options&lt;/span&gt;

  &lt;span class="n"&gt;renders_one&lt;/span&gt; &lt;span class="ss"&gt;:label&lt;/span&gt;
  &lt;span class="n"&gt;renders_one&lt;/span&gt; &lt;span class="ss"&gt;:field&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;label_options: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;object_method&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label_options&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;## app/views/components/practical_views/form/input_component.html.erb
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;'wa-stack wa-gap-3xs'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;label_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/form/input_component.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/form/input_component.rb&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/form/input_component.html.erb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/form/input_component.html.erb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This component might look trivial, but its power comes from a combination of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Restraint in its design.&lt;/li&gt;
  &lt;li&gt;Effective use of ViewComponent slots&lt;/li&gt;
  &lt;li&gt;The ability to propagate changes app-wide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The restraint is what I want to focus on most here. Where Simple Form tries to gobble up the rendering of the label and field, this component provides a slot. &lt;strong&gt;It does not care how you render the field or label, it simply puts them in the right place.&lt;/strong&gt; This means that you have flexibility for the edge cases, but a common structure.&lt;/p&gt;

&lt;p&gt;One edge-case example in Dispatcher is the “Estimated Duration” field in the Job Task form. This field has the label, but there is also a tooltip in the label that shows different examples of how the field is parsed:&lt;/p&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;app/views/components/forms/job_task_form_component.html.erb
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:estimated_duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_label&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;tooltip_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:estimated_duration_help&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-cluster"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;estimated_duration_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"Estimated Duration"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;help_icon_with_tooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;tooltip_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wa_tooltip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;for: &lt;/span&gt;&lt;span class="n"&gt;tooltip_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;table&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;caption&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;aside&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatcher.job_tasks.form.estimated_duration_popover.title"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/caption&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&amp;lt;code&amp;gt;&lt;/span&gt;1:20&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;1 hour, 20 minutes&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&amp;lt;code&amp;gt;&lt;/span&gt;90&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;90 minutes&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;th&amp;gt;&amp;lt;code&amp;gt;&lt;/span&gt;1:&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;1 hour&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_field&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:estimated_duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatcher.placeholders.estimated_duration"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;autocomplete: &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"focusout-validation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/forms/job_task_form_component.html.erb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/forms/job_task_form_component.html.erb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;input_component&lt;/code&gt; is unopinionated in what the label or field look like, we have complete freedom to customize this field in particular. It’ll be structured just like the other input components, even using the &lt;code&gt;field_title&lt;/code&gt; to maintain consistency with the label titles. The tooltip can live directly in the form because making it its own component (or subclassing the field title component) would be a waste of time and too much indirection. It’s a one-off UI element.&lt;/p&gt;

&lt;p&gt;Also note that the &lt;strong&gt;none of the customer form helpers override the critical methods from&lt;/strong&gt; &lt;code&gt;ActionView::Helpers::FormBuilder&lt;/code&gt;. The goal here is not to replace the vanilla form builder, but provide our own set of helpers that &lt;em&gt;extend&lt;/em&gt; the good parts of a form builder. This means that any Rails developer can hop in to contribute because they are still using the fundamentals; there are just some helper methods to streamline the code. And because the original form builder is untouched, we can go directly “to the metal” for truly custom cases.&lt;/p&gt;

&lt;h3 id="example-formslocationfieldcomponent-application-specific-input-component"&gt;Example: &lt;code&gt;Forms::LocationFieldComponent&lt;/code&gt; (Application-specific input component)&lt;/h3&gt;

&lt;p&gt;In Dispatcher, onsites &amp;amp; jobs both have locations. This is because a job could be pointed to one place (like a corporate HQ), while an onsite would be for a specific branch. Both use a &lt;code&gt;GooglePlace&lt;/code&gt; model, and need the same UI element for setting the location. Time for &lt;code&gt;ViewComponent&lt;/code&gt; to shine!&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/views/components/forms/location_field_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Forms::LocationFieldComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationComponent&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:object_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:location_field_name&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;location_field_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
    &lt;span class="vi"&gt;@object_method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;object_method&lt;/span&gt;
    &lt;span class="vi"&gt;@location_field_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;location_field_name&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;field_name&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;field_value&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;google_place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;
    &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;formatted_address: &lt;/span&gt;&lt;span class="n"&gt;google_place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;human_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;place_id: &lt;/span&gt;&lt;span class="n"&gt;google_place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;google_place_api_id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;google_place&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;location_field_value&lt;/span&gt;
    &lt;span class="n"&gt;google_place&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;human_address&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;## app/views/components/forms/location_field_component.html.erb
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;google_place_autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;field_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;location_field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"autocomplete-element"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="ss"&gt;autocomplete: &lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;location_field_value&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;small&lt;/span&gt; &lt;span class="na"&gt;data-formatted-address-hint&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-flank wa-gap-xs wa-quiet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatted_address_icon&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;slot&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"formatted_address"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;location_field_value&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/slot&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/small&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/forms/location_field_component.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/forms/location_field_component.rb&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/forms/location_field_component.html.erb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/forms/location_field_component.html.erb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This component is flexible: it takes any &lt;code&gt;FormBuilder&lt;/code&gt;, accepts any name the &lt;code&gt;GooglePlace&lt;/code&gt; instance uses in the model, and can use any field name you want it to. The &lt;code&gt;google-place-autocomplete&lt;/code&gt; is a Custom Element that wraps the Google Places API on the JS side.&lt;/p&gt;

&lt;p&gt;Following the Rails convention of having an &lt;code&gt;Application*&lt;/code&gt; Base class, it’s easy for Dispatcher to extend the &lt;code&gt;PracticalViews&lt;/code&gt; framework with its own custom inputs:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;## app/form_builders/new_application_form_builder.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NewApplicationFormBuilder&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FormBuilders&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;WebAwesome&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;location_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;location_field_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Forms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LocationFieldComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;f: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;object_method: &lt;/span&gt;&lt;span class="n"&gt;object_method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;location_field_name: &lt;/span&gt;&lt;span class="n"&gt;location_field_name&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Bringing it all together, the flexibility of our &lt;code&gt;InputComponent&lt;/code&gt; means that this custom component fits perfectly into our forms as if it was there all along:&lt;/p&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;app/views/components/forms/job_form_component.html.erb
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label_options: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;for: &lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:location&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_label&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;location_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"Address"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_field&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;location_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;object_method: :google_place&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;location_field_name: :location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Using these approaches; you can build forms that behave consistently, are maintainable by everyone on the team, and don’t get in your way.&lt;/p&gt;

&lt;h2 id="sane-icon-management-with-an-iconset-poro"&gt;Sane icon management with an &lt;code&gt;IconSet&lt;/code&gt; PORO&lt;/h2&gt;

&lt;p&gt;Maintaining &amp;amp; applying icons across a complex project can be a maintenance nightmare. A delightful app has numerous icons, with good variety, and keeps the icon set refreshed &amp;amp; looking sharp.&lt;/p&gt;

&lt;p&gt;Some strategies used for getting icons into the view are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Shared partials, which are a pain to call and hard to maintain&lt;/li&gt;
  &lt;li&gt;Putting all the icons in a single, &lt;code&gt;IconHelper&lt;/code&gt; helper; but that pollutes the view namespace and is a flat hierarchy that is hard to extend or adjust.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An icon makes a great component, especially Font Awesome icons given the number of families, variants, and customization options:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/views/components/practical_views/icon_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PracticalViews::IconComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseComponent&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Serializers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;

  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:fixed_width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:options&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;attributes&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"family"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"variant"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"fixed_width"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"label"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"options"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;variant: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;fixed_width: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;family&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;family&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fixed_width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fixed_width&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wa_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"family"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"variant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"fixed-width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fixed_width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/icon_component.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/views/components/practical_views/icon_component.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problems with an &lt;code&gt;IconComponent&lt;/code&gt; on its own are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Applying the same icon across an app, while keeping it maintainable to adjust said icon&lt;/li&gt;
  &lt;li&gt;The verbosity of calling &lt;code&gt;render IconComponent.new(name: ...)&lt;/code&gt; any time you want to put an icon on a page&lt;/li&gt;
  &lt;li&gt;Setting up presets&lt;/li&gt;
  &lt;li&gt;Aliasing the same icon for multiple contexts so you have to think less&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; create a series of helper methods that return these icons in an &lt;code&gt;IconHelper&lt;/code&gt; helper. &lt;strong&gt;But&lt;/strong&gt;, a better approach is to borrow a page from the Apple Ecosystem, with the idea of “Asset Bundles.”&lt;/p&gt;

&lt;p&gt;In iOS/Mac/etc. apps, Asset Bundles are a collection of images/files/color swatches that should exist &lt;em&gt;alongside&lt;/em&gt; code, but are not code in of themselves. They should be bundled as part of the final binary, and referenced in code, but are treated like a box you pull from the shelf.&lt;/p&gt;

&lt;p&gt;I experimented with the API, and finally came up with something that is straightforward, yet highly extendable: the idea of an &lt;code&gt;IconSet&lt;/code&gt;. This is a PORO that:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Encapsulates the process of returning an &lt;code&gt;IconComponent&lt;/code&gt; instance.&lt;/li&gt;
  &lt;li&gt;Has a set of easily accessible presets for how an icon should be rendered.&lt;/li&gt;
  &lt;li&gt;Provides helper methods to render specific icons based on &lt;em&gt;intention/purpose&lt;/em&gt; rather than the specific icon&lt;/li&gt;
  &lt;li&gt;Icons can be aliased, replaced, segmented using subclassing.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/lib/practical_views/icon_set.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PracticalViews::IconSet&lt;/span&gt;
  &lt;span class="nc"&gt;IconDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:method_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:icon_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:preset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="no"&gt;Preset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:variant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="no"&gt;PRESETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;brand: &lt;/span&gt;&lt;span class="no"&gt;Preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;family: :brands&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;variant: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;duotone: &lt;/span&gt;&lt;span class="no"&gt;Preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;family: :duotone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;variant: :solid&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;regular: &lt;/span&gt;&lt;span class="no"&gt;Preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;family: :classic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;variant: :regular&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;solid: &lt;/span&gt;&lt;span class="no"&gt;Preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;family: :solid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;variant: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="ss"&gt;kit: &lt;/span&gt;&lt;span class="no"&gt;Preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;family: :kit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;variant: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presets&lt;/span&gt;
    &lt;span class="no"&gt;PRESETS&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IconComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;define_singleton_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="ss"&gt;_icon"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;preset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;family: &lt;/span&gt;&lt;span class="n"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;variant: &lt;/span&gt;&lt;span class="n"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define_icons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon_definitions&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;icon_definitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;icon_definition&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;define_singleton_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;method_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;preset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;presets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="ss"&gt;family: &lt;/span&gt;&lt;span class="n"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;family&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;variant: &lt;/span&gt;&lt;span class="n"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;icon_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icon_name&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;define_icons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon_definitions: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="no"&gt;IconDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;method_name: :user_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;icon_name: :user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;preset: :duotone&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="no"&gt;IconDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;method_name: :save_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;icon_name: :"floppy-disk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;preset: :duotone&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="no"&gt;IconDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;method_name: :filters_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;icon_name: :filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;preset: :duotone&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/lib/practical_views/icon_set.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/lib/practical_views/icon_set.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dispatcher inherits from this framework-level IconSet (which will be common across all apps), to provide an &lt;code&gt;ApplicationIconSet&lt;/code&gt;&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/lib/application_icon_set.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationIconSet&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IconSet&lt;/span&gt;
  &lt;span class="n"&gt;define_icons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon_definitions: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="no"&gt;IconDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;method_name: :job_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;icon_name: :"house-building"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;preset: :duotone&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offsite_icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;organization_icon&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edit_onsite_icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onsite_icon&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;job_status_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:active&lt;/span&gt;
      &lt;span class="n"&gt;active_icon&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:archived&lt;/span&gt;
      &lt;span class="n"&gt;archived_icon&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/lib/application_icon_set.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/app/lib/application_icon_set.rb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To streamline using it, the &lt;code&gt;ApplicationHelper&lt;/code&gt; and &lt;code&gt;ApplicationComponent&lt;/code&gt; have an &lt;code&gt;icon_set&lt;/code&gt; method that returns the class:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ApplicationHelper&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ElementHelper&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;icon_set&lt;/span&gt;
    &lt;span class="no"&gt;ApplicationIconSet&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseComponent&lt;/span&gt;
  &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="ss"&gt;:icon_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :helpers&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;


&lt;span class="no"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rendering&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;simple&lt;/span&gt; &lt;span class="ss"&gt;as:

&lt;/span&gt;&lt;span class="o"&gt;~~~&lt;/span&gt;&lt;span class="n"&gt;erb&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= render icon_set.user_icon %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And because we return an &lt;em&gt;instance&lt;/em&gt; of the &lt;code&gt;IconComponent&lt;/code&gt;, we can do things like pass it along to another ViewComponent, or as part of the flash hash to be serialized for rendering a Toast component:&lt;/p&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_name_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/views/components/practical_views/flash_messages_component.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color_variant&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;default_icon&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;default_icon&lt;/span&gt;
  &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_indifferent_access&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:icon&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;


  &lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ToastComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;color_variant: &lt;/span&gt;&lt;span class="n"&gt;color_variant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;icon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;IconComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;symbolize_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_icon&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;message&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/practical_views/flash_messages_component.rb#L3"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/practical_views/flash_messages_component.rb#L3&lt;/a&gt;&lt;/p&gt;

&lt;h2 id="running-the-test-suite-against-both-uis"&gt;Running the test suite against both UIs&lt;/h2&gt;

&lt;p&gt;Once you’ve gotten all the views ported over, you can now start using your test suite to test against both versions of the UI.&lt;/p&gt;

&lt;p&gt;The easiest way to handle this is to have the test suite check for an environment variable, which sets the feature flag globally before each test run:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test/test_helper.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;WebAwesomeTest&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;web_awesome?&lt;/span&gt;
    &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"WEB_AWESOME"&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;span class="n"&gt;setup&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;WebAwesomeTest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;web_awesome?&lt;/span&gt;
    &lt;span class="no"&gt;Flipper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:web_awesome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActionDispatch::IntegrationTest&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;WebAwesomeTest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;web_awesome?&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;PatchedFlashAssertions&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;PracticalFramework&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestHelpers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FlashAssertions&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can then set up your CI to run the tests for both versions of the UI, and make patches as needed to adjust the test behavior.  In short: you should take the same approach as dual-booting Rails versions to incrementally upgrade.&lt;/p&gt;

&lt;p&gt;Relevant files:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/test/test_helper.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/test/test_helper.rb&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/.circleci/config.yml#L149-L179"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/.circleci/config.yml#L149-L179&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/test/controllers/organizations/jobs_controller_test.rb#L24-L30"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/main/test/controllers/organizations/jobs_controller_test.rb#L24-L30&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id="tips-and-other-items-i-couldnt-find-a-place-for"&gt;Tips and other items I couldn’t find a place for&lt;/h1&gt;

&lt;h2 id="viewcomponent-tips"&gt;ViewComponent Tips&lt;/h2&gt;

&lt;p&gt;In no particular order, here are some things I found helpful as I wrote out the ViewComponents for my framework, and Dispatcher in particular.&lt;/p&gt;

&lt;h3 id="viewcomponent-configuration"&gt;ViewComponent configuration&lt;/h3&gt;

&lt;p&gt;Below is the configuration I used for ViewComponent:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/application.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view_component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view_component_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app/views/components"&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eager_load_paths&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app/views/components"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view_component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view_component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture_compatibility_patch_enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view_component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_preview_layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"component_preview"&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view_component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preview_controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CustomViewComponentsController"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I want my view components to be logically structured with the rest of the view code, the compatibility patch needs to be enabled, the generators should always encourage me to fill out the previews, and you need custom preview layouts &amp;amp; controllers for any non-trivial app.&lt;/p&gt;

&lt;h3 id="forms-should-be-components-and-live-in-the-forms-namespace"&gt;Forms should be components, and live in the &lt;code&gt;Forms::&lt;/code&gt; namespace&lt;/h3&gt;

&lt;p&gt;Making ViewComponents to encapsulate your forms treats them with the same level of code quality you give to the backend code. It also helps you:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Prototype a form using Lookbook.&lt;/li&gt;
  &lt;li&gt;DRY up your forms with your standard OOP tool belt.&lt;/li&gt;
  &lt;li&gt;Scan through your application when debugging or getting oriented with a particular feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also helps keep your views lighter because forms generate a lot of specialized markup. You’ve likely taken a similar approach with form partials in your existing code, but ViewComponents are better encapsulations of the complexity.&lt;/p&gt;

&lt;h3 id="dont-over-generalize"&gt;Don’t over-generalize!&lt;/h3&gt;

&lt;p&gt;A big problem with building component libraries is the desire to over-generalize. It works great in demos (and that’s how folks tend to show off the libraries), but it ends up being a ton of maintenance overhead and frankly annoying code to deal with.&lt;/p&gt;

&lt;p&gt;If you have something that needs to be peppered everywhere, like “Icon + Text”, that should be a helper method. You don’t need a full component for it because that just clutters your actual view code. My first pass at standardizing “Icon + Text” was actually a ViewComponent, but for terseness &amp;amp; simplicity, I refactored it back to a standard Helper method:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;icon_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"wa-flank wa-gap-3xs icon-text"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;safe_join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ViewComponents are a powerful tool for abstracting and cleaning up view code, but don’t abuse them! Helpers are just a good a tool for small cases.&lt;/p&gt;

&lt;h3 id="define-your-own-abstraction-layer-based-on-the-intentions-that-make-sense-to-you"&gt;Define your own abstraction layer, based on the intentions that make sense &lt;em&gt;to you&lt;/em&gt;&lt;/h3&gt;

&lt;p&gt;If you’re using a good frontend component library, it already has a comprehensive language to abstract away problems for you. Rather than trying to map that 1:1 with your ViewComponents, you should instead &lt;strong&gt;define the abstractions that make sense for you&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s better to define your own ergonomics, and let the frontend framework grow and adapt as needed. You’ll be able to adapt its changes to your own needs, finding the right translation between each layer, without muddying up the rest of the logic in your application layer.&lt;/p&gt;

&lt;h3 id="time-to-mix-and-grab-from-phlex"&gt;Time to &lt;code&gt;mix&lt;/code&gt; and &lt;code&gt;grab&lt;/code&gt; from Phlex&lt;/h3&gt;

&lt;p&gt;Phlex has two great helpers, &lt;code&gt;mix&lt;/code&gt; and &lt;code&gt;grab&lt;/code&gt;; whose source is easily available. I’d highly recommend &lt;a href="https://www.phlex.fun/sgml/helpers.html"&gt;reading their documentation&lt;/a&gt;, and adding them to your project.&lt;/p&gt;

&lt;h3 id="slots-over-subclasses-generally"&gt;Slots over Subclasses, generally&lt;/h3&gt;

&lt;p&gt;One of ViewComponent’s power features is slots. They allow you to customize behavior for one-off/specialized cases, without needing to go through the hassle of managing subclasses or generalizing a component.&lt;/p&gt;

&lt;p&gt;We went over this some with the &lt;code&gt;InputComponent&lt;/code&gt;, but let’s also look at the &lt;code&gt;Organization::NoteComponent&lt;/code&gt;:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/views/components/v2/organization/note_component.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;V2::Organization::NoteComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationComponent&lt;/span&gt;
  &lt;span class="n"&gt;renders_one&lt;/span&gt; &lt;span class="ss"&gt;:resource_link&lt;/span&gt;
  &lt;span class="n"&gt;renders_one&lt;/span&gt; &lt;span class="ss"&gt;:actions&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:note&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="vi"&gt;@note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;# app/views/components/v2/organization/note_component.html.erb
&lt;span class="nt"&gt;&amp;lt;wa-card&lt;/span&gt; &lt;span class="na"&gt;with-footer&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-footer note"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-stack wa-gap-0 tiptap-document"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TiptapDocumentComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;document: &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tiptap_document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;


  &lt;span class="nt"&gt;&amp;lt;footer&lt;/span&gt; &lt;span class="na"&gt;slot=&lt;/span&gt;&lt;span class="s"&gt;"footer"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-stack"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-split"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-cluster"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;UserNameComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;original_author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resource_link?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;resource_link&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-stack wa-gap-0 wa-size-s"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;written &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RelativeTimeComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;time: &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updated_at&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;updated &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RelativeTimeComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;time: &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;actions?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/wa-card&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/v2/organization/note_component.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/v2/organization/note_component.rb&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/v2/organization/note_component.html.erb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/v2/organization/note_component.html.erb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I want to &lt;em&gt;optionally&lt;/em&gt; add actions (such as editing/deleting a note), or a link to the resource the note is tied to. Rather than creating a myriad of subclasses (a maintenance nightmare), I can define the slots right where they’re needed in the view code:&lt;/p&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;# app/views/components/organization/onsite/notes_component.html.erb
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-stack"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;icon_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notes_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="s2"&gt;"Notes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-stack"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;onsite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chronological&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;V2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Organization&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NoteComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;note: &lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_actions&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-size-s wa-split"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="n"&gt;edit_organization_note_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"wa-button"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
              &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;icon_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notes_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatcher.notes.edit_note.link_title"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

            &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_note_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s1"&gt;'wa-danger'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;confirm: &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatcher.notes.delete_note.confirmation_message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
              &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;icon_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;icon_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_note_icon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dispatcher.notes.delete_note.link_title"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
            &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"add-note-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;Forms&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Onsite&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AddNoteFormComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;form: &lt;/span&gt;&lt;span class="n"&gt;note_form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Like with subclasses, you can easily abuse slots. But when working with ViewComponents, I’d reach for a Slot until you truly need a subclass.&lt;/p&gt;

&lt;h2 id="use-call-for-basic-components-tag-helpers-for-complex-attribute-serializing"&gt;Use &lt;code&gt;call&lt;/code&gt; for basic components, &lt;code&gt;tag.*&lt;/code&gt; helpers for complex attribute serializing&lt;/h2&gt;

&lt;p&gt;For basic components that don’t require a lot of markup, I forgo the sidecar template file in favor of building the template directly in the component class. In particular, I use the &lt;code&gt;tag.*&lt;/code&gt; helpers here (and wherever I am writing elements that mixin hash-based attributes) to ensure proper markup.&lt;/p&gt;

&lt;p&gt;Some examples:&lt;/p&gt;

&lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/views/components/practical_views/form/option_label_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PracticalViews::Form::OptionLabelComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;PracticalViews&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseComponent&lt;/span&gt;
  &lt;span class="n"&gt;renders_one&lt;/span&gt; &lt;span class="ss"&gt;:title&lt;/span&gt;
  &lt;span class="n"&gt;renders_one&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:options&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;


  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;mix&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"wa-stack wa-size-s wa-gap-0"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;safe_join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;small&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"wa-quiet"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;# app/views/components/practical_views/navigation/breadcrumbs_component.html.erb
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wa_breadcrumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;finalized_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;truncate_middle?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;truncate_start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;build_crumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;crumb: &lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;wa-breadcrumb-item&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;wa-dropdown&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;wa-button&lt;/span&gt; &lt;span class="na"&gt;slot=&lt;/span&gt;&lt;span class="s"&gt;"trigger"&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="na"&gt;appearance=&lt;/span&gt;&lt;span class="s"&gt;"filled"&lt;/span&gt; &lt;span class="na"&gt;pill&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;wa-icon&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"More options"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"ellipsis"&lt;/span&gt; &lt;span class="na"&gt;variant=&lt;/span&gt;&lt;span class="s"&gt;"solid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/wa-icon&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/wa-button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wa-stack wa-gap-xs dropdown-navigation"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navigation-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;truncated_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;href: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
          &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/wa-dropdown&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/wa-breadcrumb-item&amp;gt;&lt;/span&gt;

    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;truncate_end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;build_crumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;crumb: &lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;breadcrumb_trail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;build_crumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;crumb: &lt;/span&gt;&lt;span class="n"&gt;crumb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/practical_views/form/option_label_component.rb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/practical_views/form/option_label_component.rb&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/practical_views/navigation/breadcrumbs_component.html.erb"&gt;https://github.com/practical-computer/flip-on-the-awesome-snapshot/blob/2721248473498c3bcd8e71a8fe14ae6bcc7ca4c6/app/views/components/practical_views/navigation/breadcrumbs_component.html.erb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="dont-worry-about-testing-until-everything-has-been-ported"&gt;Don’t worry about testing until everything has been ported&lt;/h3&gt;

&lt;p&gt;There will be so much in flux as the new variants &amp;amp; components get built out that testing at this stage is a waste of cycles. Likewise, the porting is already a massive slog, so you don’t want to introduce even more slog to that process and lose momentum&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additionally: You don’t need a preview or tests for everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ViewComponent makes a point to emphasize previews &amp;amp; tests, leading you to believe that you need them for &lt;em&gt;everything&lt;/em&gt;. However, even though I’m someone who swears by testing extensively, I think unit tests on ViewComponents are generally overkill.&lt;/p&gt;

&lt;p&gt;Testing is a game of tradeoffs. You sacrifice sprints for overall endurance, quick code for testable code, “fun” for stability. UIs generate &lt;strong&gt;a lot&lt;/strong&gt; of code, but it’s also surfaced to the user and problems are easily recognized by them.&lt;/p&gt;

&lt;p&gt;With a good test suite, you also get decent coverage on your components because you’re already rendering them as part of the integration or system tests.&lt;/p&gt;

&lt;p&gt;If I’m thinking of where to burn my testing &amp;amp; dev budgets, I’d much rather spend them on testing at other layers, or improving my development &amp;amp; Lookbook data via Oaken.&lt;/p&gt;

&lt;p&gt;Aim for previews that load painlessly in Lookbook, test the truly critical components if it helps you sleep better. But don’t lose sight of shipping a better UI for people.&lt;/p&gt;

&lt;h2 id="fighting-boredom"&gt;Fighting Boredom&lt;/h2&gt;

&lt;p&gt;Tackling a UI overhaul is a &lt;em&gt;slow&lt;/em&gt; process, so you want to make sure to pace yourself, avoid burnout, and prevent boredom.&lt;/p&gt;

&lt;h3 id="a-fresh-coat-of-paint"&gt;A fresh coat of paint&lt;/h3&gt;

&lt;p&gt;A good way to fight boredom, and test the flexibility of your components, is to swap out the theme for your frontend framework. It makes the work less tedious and makes sure you aren’t drifting too far from what the framework has defined as guidelines.&lt;/p&gt;

&lt;p&gt;You might also find a new theme that works better for your app as it gets closer to release!&lt;/p&gt;

&lt;h3 id="switch-to-other-projects"&gt;Switch to other projects&lt;/h3&gt;

&lt;p&gt;If you get tired of the process of porting views into components, pick up a small ticket to avoid falling asleep at the wheel.&lt;/p&gt;

&lt;h3 id="keep-getting-prs-merged-in"&gt;Keep getting PRs merged in&lt;/h3&gt;

&lt;p&gt;Having the code live on &lt;code&gt;main&lt;/code&gt; avoids the feeling of toiling away on something that won’t see the light of day. If it’s on &lt;code&gt;main&lt;/code&gt;, other developers can see it and give feedback. You can show it off to customers in sneak peeks, and get everyone excited about the fresh coat of paint.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:thomascannon.me,2024-08-10:/guides/fixing-the-rails-networking-stack.html</id>
    <title type="html">Fixing the Rails 7+ networking stack</title>
    <published>2024-08-10T00:00:00Z</published>
    <updated>2024-08-10T00:00:00Z</updated>
    <link rel="alternate" href="https://thomascannon.me/guides/fixing-the-rails-networking-stack.html" type="text/html"/>
    <content type="html">&lt;h1 id="fixing-the-rails-7-networking-stack"&gt;Fixing the Rails 7+ networking stack&lt;/h1&gt;

&lt;p&gt;Let’s talk networking stacks. Which, I know, boring, but it’s foundational!&lt;/p&gt;

&lt;h2 id="the-problem-and-general-stack"&gt;The problem and general stack&lt;/h2&gt;

&lt;p&gt;I want Little CRM to be as resilient as possible, which means making it progressively enhance through as much of the gamut as possible:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No JS loaded.&lt;/li&gt;
  &lt;li&gt;JS is partially loaded (but broken).&lt;/li&gt;
  &lt;li&gt;JS is loaded, but Websockets are busted.&lt;/li&gt;
  &lt;li&gt;Websockets are functional, giving us real-time updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given this, the network stack needs the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Support for standard HTTP request/responses, &lt;em&gt;even if they’re not ideal&lt;/em&gt;. Progressive enhancement is about &lt;strong&gt;allowing people to keep using your app if things are non-ideal (but still functional), not a 1:1 replication top-to-bottom.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;A sensible client-side API for handling fetch/AJAX-based requests.&lt;/li&gt;
  &lt;li&gt;A lightweight real-time update layer using Websockets, as minimally impactful as possible in both client-side code &amp;amp; the app’s dependence on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generally speaking, the stack looks like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Write the page markup semantically using native form elements &amp;amp; links.&lt;/li&gt;
  &lt;li&gt;Use &lt;a href="https://mrujs.netlify.app"&gt;Mrujs&lt;/a&gt; to progressively enhance the forms &amp;amp; links that should use fetch/AJAX requests.&lt;/li&gt;
  &lt;li&gt;Take advantage of Rails’ &lt;code&gt;respond_to&lt;/code&gt; method to provide both ideal (fetch/AJAX) and fallback HTML responses with minimal code duplication.&lt;/li&gt;
  &lt;li&gt;Use &lt;a href="https://cableready.stimulusreflex.com/"&gt;CableReady&lt;/a&gt; as the response protocol for fetch/AJAX requests (using &lt;a href="https://cableready.stimulusreflex.com/guide/cable-car.html"&gt;CableCar&lt;/a&gt;), while &lt;em&gt;also&lt;/em&gt; using it to broadcast changes &lt;strong&gt;using the same payload for both.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Adding light event handlers in the browser to handle edge cases such as double-prepending &amp;amp; augmenting form submission events to autofocus &amp;amp; reset forms. Thanks to the beauty of Mrujs &amp;amp; CableReady’s designs, only 3 were needed!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first question is, “Why not Hotwire/Turbo?”. I mean, this is a Rails app, after all!&lt;/p&gt;

&lt;h2 id="turbo-adds-excessive-complexity-and-reimplements-the-browser"&gt;Turbo adds excessive complexity and reimplements the browser&lt;/h2&gt;

&lt;p&gt;After using Hotwire/Turbo in its various iterations, observing folks talk about it, and looking at the larger context of the library, the following problems made that not a viable option here.&lt;/p&gt;

&lt;h3 id="it-reimplements-the-core-of-the-browser"&gt;It reimplements the core of the browser&lt;/h3&gt;

&lt;p&gt;Turbo relies heavily on DOM morphing, a core tenant being “write your server-rendered pages and let Turbo morph the DOM &amp;amp; browser history for SPA-like responsiveness”. This introduces major &amp;amp; frustrating points of failure for two of the most foundational parts of a web browser.&lt;/p&gt;

&lt;p&gt;Trying to sidestep how the browser handles page loading and its history, using morphing and heavily mucking with the History API, introduces a ton of gotchas. When you factor in:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Browser caching&lt;/li&gt;
  &lt;li&gt;HTTP2/3&lt;/li&gt;
  &lt;li&gt;Network speeds&lt;/li&gt;
  &lt;li&gt;The complexity of browser engines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think it’s better to have your standard request/response flow-either as standard page navigation or by returning JSON that some lightweight JS handlers can use to update &lt;em&gt;some&lt;/em&gt; of the existing DOM. Instead, the Turbo approach tries to programmatically diff the changes and morph the page to match what has changed, then change the browser’s history. It’s the same class of problem as a virtual DOM. Even if you’re using something like morphdom, the principle of how it operates is the same.&lt;/p&gt;

&lt;p&gt;While you can opt out of these behaviors, you have to explicitly do so with a boatload of &lt;code&gt;meta&lt;/code&gt; tags, configuration, and remembering to do so. &lt;strong&gt;Turbo &lt;em&gt;wants&lt;/em&gt; to do all this work; it thinks this is the way to handle behavior.&lt;/strong&gt; Which I disagree with because most of the conversations around Turbo I see outside CodePens &amp;amp; simple examples are seasoned programmers asking why it’s not working as expected. And if the default behavior of a framework as critical as your frontend stack causes that, then &lt;em&gt;maybe its approach is fundamentally flawed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CableReady does have DOM morphing and browser history manipulation, but they are &lt;strong&gt;opt-in&lt;/strong&gt;. It’s clear when those behaviors occur because you choose to enable them. You only write code for the behaviors you want—&lt;em&gt;not to rip out the behaviors that cause problems&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id="turbo-streams-are-not-an-easily-grokableadjustable-protocol"&gt;Turbo Streams are not an easily grokable/adjustable protocol&lt;/h3&gt;

&lt;p&gt;Turbo Streams &lt;em&gt;are&lt;/em&gt; how Turbo allows you to send incremental changes as part of remote form submissions, but they’re difficult to use outside of the very basic cases.&lt;/p&gt;

&lt;p&gt;For those who aren’t familiar with them, this is an example of Turbo Streams from the docs:&lt;/p&gt;

&lt;div class="language-html highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"append"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"messages"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      This div will be appended to the element with the DOM ID "messages".
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"prepend"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"messages"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      This div will be prepended to the element with the DOM ID "messages".
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"replace"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"message_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      This div will replace the existing element with the DOM ID "message_1".
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"after"&lt;/span&gt; &lt;span class="na"&gt;targets=&lt;/span&gt;&lt;span class="s"&gt;"input.invalid_field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- The contents of this template will be added after the
    all elements that match "inputs.invalid_field". --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Incorrect&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In essence, there are a series of custom elements that encapsulate the changes to the DOM &amp;amp; browser you want to perform.&lt;/p&gt;

&lt;p&gt;I can see how you arrive at this API design! It’s not awful, but it could be so much easier &amp;amp; extendable.&lt;/p&gt;

&lt;p&gt;Like CableCar, the protocol for CableReady operations.&lt;/p&gt;

&lt;p&gt;Let’s look at a similar payload of CableCar operations:&lt;/p&gt;

&lt;div class="language-json highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;div id=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;message_1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;This div will be appended to the element with the DOM ID &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;messages&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;.&amp;lt;/div&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"append"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;div id=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;message_1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;This div will be prepended to the element with the DOM ID &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;messages&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;.&amp;lt;/div&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prepend"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#message_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;div id=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;message_1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;This div will replace the existing element with the DOM ID &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;message_1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;.&amp;lt;/div&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"selector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"input.invalid_field"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"select_all"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"afterend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;span&amp;gt;Incorrect&amp;lt;/span&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"insertAdjacentHtml"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;details&gt;
  &lt;summary&gt;See the code that generates this JSON&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="s2"&gt;"#messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"..."", formats: [:html])
)

cable_car.prepend(
  selector: "&lt;/span&gt;&lt;span class="c1"&gt;#messages",&lt;/span&gt;
  &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"..."", formats: [:html])
)

cable_car.replace(
  selector: "&lt;/span&gt;&lt;span class="c1"&gt;#message_1",&lt;/span&gt;
  &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"..."", formats: [:html])
)

cable_car.insert_adjacent_html(
  selector: 'input.invalid_field',
  select_all: true,
  position: 'afterend',
  html: '&amp;lt;span&amp;gt;Incorrect&amp;lt;/span&amp;gt;'
)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;It’s easily understandable; you can explain it in a sentence: there is a JSON array of operations, and you can augment the default behavior with extra JSON data.&lt;/p&gt;

&lt;p&gt;Because it’s an extremely simple protocol, it can be reused across transmission mechanisms with minimal cognitive overhead. In my case, I’m using:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Mrujs’s CableCar plugin&lt;/li&gt;
  &lt;li&gt;CableReady’s default ActionCable interface&lt;/li&gt;
  &lt;li&gt;A custom channel I’ve defined&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All use the same protocol, avoiding duplicate code &amp;amp; juggling different mental models.&lt;/p&gt;

&lt;p&gt;I can’t emphasize enough how useful this is: &lt;strong&gt;one protocol, easily understandable, flexible enough to be incorporated into a wide variety of contexts.&lt;/strong&gt;&lt;/p&gt;

&lt;figure&gt;
  &lt;img src="/assets/networking-stack/CableCar protocol usage.png" alt="A diagram of how CableCar acts as the single transmission protocol for updates across Mrujs, CableReady's default channels, and custom ActionCable channels" /&gt;
  &lt;figcaption&gt;
  &lt;p&gt;One protocol, easily understandable, flexible enough to be incorporated into a wide variety of contexts.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Coming back to that point about augmenting the protocol, I have a concrete example of this: prepend only if a selector is missing.&lt;/p&gt;

&lt;p&gt;The use case is straightforward:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;We want people to be able to post to a timeline.&lt;/li&gt;
  &lt;li&gt;We want updates to show up immediately on the new timeline.&lt;/li&gt;
  &lt;li&gt;We want any real-time updates for new posts added by others or in another tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;figure&gt;
  &lt;video controls="" width="480" height="300" poster="/assets/networking-stack/CableReady Realtime Update Thumbnail.png"&gt;
    &lt;source src="/assets/networking-stack/CableReady Realtime Updates Demo.mp4" type="video/mp4" /&gt;
  &lt;/video&gt;
  &lt;figcaption&gt;
  &lt;p&gt;A demo showing off realtime posts via CableCar and CableReady, using the same JSON payload for both&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;details open=""&gt;
  &lt;summary&gt;Here's the Microposts Controller&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/organizations/microposts_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Organizations::MicropostsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Organizations&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CableReady&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Broadcaster&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;TimelineEdgeContextLoading&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;TimelineEntryCableCar&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;FlashMessagesCableCar&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Organization&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MicropostForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_micropost_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;

    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'little_crm.microposts.created_message'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flash_success_with_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost_icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;organization_micropost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost&lt;/span&gt;
    &lt;span class="c1"&gt;# Helper method that determines what URL we should redirect to, centralized for easier testing&lt;/span&gt;
    &lt;span class="n"&gt;redirect_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url_for_root_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;root_context: &lt;/span&gt;&lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;creation_postable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                &lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;autofocus_micropost: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                                                &lt;span class="ss"&gt;fallback: &lt;/span&gt;&lt;span class="n"&gt;organization_micropost_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                                                     &lt;span class="n"&gt;organization_micropost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                               &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;redirect_url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cable_ready&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;render_flash_messages_to_cable_car_and_clear&lt;/span&gt;

        &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeline_node&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outward_edges&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
          &lt;span class="n"&gt;edge_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Timeline&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EdgeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;edge: &lt;/span&gt;&lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;root_context: &lt;/span&gt;&lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;creation_postable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;cable_car_add_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="n"&gt;edge_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cable_ready: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordInvalid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ValidationError&lt;/span&gt;
    &lt;span class="n"&gt;default_respond_to_model_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;html_action: :new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="c1"&gt;# Only allow a list of trusted parameters through.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;organization_micropost_params&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:organization_micropost&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;:content&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;current_user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;current_organization: &lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;creation_postable: &lt;/span&gt;&lt;span class="no"&gt;Organization&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Micropost&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Postable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_from_sgid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;sgid: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:organization_micropost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                                                                            &lt;span class="ss"&gt;:creation_postable&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/details&gt;

&lt;details open=""&gt;
  &lt;summary&gt;The micropost form&lt;/summary&gt;

  &lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;# app/views/organizations/microposts/_form.html.erb
&lt;span class="c"&gt;&amp;lt;%# locals: (form:, creation_postable: nil, timeline_edge_context: nil) -%&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;%# The `creation_postable` is set to the organization in this example, since we're posting on the Organization's timeline %&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;input_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:content_text_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;application_form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;url_for&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;organization_id: &lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                           &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;reset_on_ajax_success: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;focus_on_ajax_success: &lt;/span&gt;&lt;span class="n"&gt;id_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
                          &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;creation_postable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hidden_field&lt;/span&gt; &lt;span class="ss"&gt;:creation_postable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;creation_postable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postable_sgid&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/details&gt;

&lt;details open=""&gt;
  &lt;summary&gt;And some of the concerns to DRY up the code&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/concerns/flash_messages_cable_car.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FlashMessagesCableCar&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_flash_messages_to_cable_car_and_clear&lt;/span&gt;
    &lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outer_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="s2"&gt;".notification-messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"/layouts/snippets/flash_messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;formats: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:alert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:notice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# app/controllers/concerns/timeline_entry_cable_car.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TimelineEntryCableCar&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cable_car_add_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="no"&gt;CableCarAndBroadcast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend_new_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/details&gt;

&lt;details open=""&gt;
  &lt;summary&gt;The concern that prepares the CableCar response while also broadcasting the same payload&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/broadcasters/cable_car_and_broadcast/timeline.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CableCarAndBroadcast::Timeline&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;CableReady&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Broadcaster&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;helpers&lt;/span&gt;
    &lt;span class="no"&gt;ApplicationController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;helpers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend_new_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;html_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CableCar&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TimelineEntryController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_new_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;existing_selector: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_gid_param&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeline_dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;root_context: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root_context&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html_string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;cableready_stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cable_ready&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;TimelineStreamChannel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cableready_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cableready_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;organization_stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# I kept this as a controller so that the mental modeling of Controller =&amp;gt; View rendering is retained&lt;/span&gt;
&lt;span class="c1"&gt;# app/controllers/cable_car/timeline_entry_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CableCar::TimelineEntryController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
 &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_new_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"/timelines/entry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;edge: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;root_context: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root_context&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;formats: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;We use the same payload to prepend the post in both the AJAX response and the ActionCable broadcast, giving us consistent updates and DRY code. But a double posting problem presented itself.&lt;/p&gt;

&lt;p&gt;To recap the problem:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You can post on a timeline, which is done via a fetch request.&lt;/li&gt;
  &lt;li&gt;That returns a CableCar set of responses, prepending the post to the timeline.&lt;/li&gt;
  &lt;li&gt;But since we also want the timeline to receive real-time updates, the browser will also get the broadcasted update since it’s subscribed to the timeline’s broadcast channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;figure&gt;
  &lt;img src="/assets/networking-stack/CableCar double posting.png" alt="A diagram showing how a standard HTTP request and a Websockets broadcast using the same payload can lead to a double posting problem" /&gt;
  &lt;figcaption&gt;
  &lt;p&gt;Notice how Browser 1 is going to receive both the HTTP response and the websockets broadcast.&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;So, we need to throw away the prepend operation that comes &lt;em&gt;last&lt;/em&gt; (assuming both arrive, which you can’t assume!)&lt;/p&gt;

&lt;p&gt;Because CableReady is well-designed, it has events for each stage of the lifecycle for each operation. And because it &lt;em&gt;also&lt;/em&gt; gives you the flexibility to add arbitrary data for existing operations, this became a basic fix: a global utility function that is hooked into an event listener:&lt;/p&gt;

&lt;div class="language-js highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throwAwayExistingCableReadyPrependEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;existingSelector&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;existingSelector&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingSelector&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;throwAwayExistingCableReadyPrependEventHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;practical-framework&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`cable-ready:before-prepend`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;throwAwayExistingCableReadyPrependEventHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s all it took! And the mental model is straightforward.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Operations are self-contained, so they operate independently.&lt;/li&gt;
  &lt;li&gt;Because they’re a common datatype (an array of objects), the idea of a series of lifecycle events that are dispatched is a natural extension.&lt;/li&gt;
  &lt;li&gt;Because they are that common, almost primitive datatype, event handlers can easily inspect their details and, well, handle the event!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So with that sprinkling of code, I’ve got real-time updates that can reuse the exact same payloads as the AJAX request/response that is tried and true.&lt;/p&gt;

&lt;h2 id="designing-plain-html-fallbacks"&gt;Designing plain HTML fallbacks&lt;/h2&gt;

&lt;p&gt;Making sure an app is &lt;em&gt;functional&lt;/em&gt; when a regular HTTP request is made is a core tenant of progressive enhancement.&lt;/p&gt;

&lt;p&gt;We’ve all run into it before—you click a button or submit a form, and either:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nothing seems to happen.&lt;/li&gt;
  &lt;li&gt;You get a weird JSON payload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s immensely frustrating and a solvable problem!! But at its core, &lt;strong&gt;folks are still not designing well enough for the underlying HTML responses.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This comes down to folks throwing up their hands when it comes to progressive enhancement. If a request is made via AJAX, teams don’t make an HTML backup. Developers don’t think about how to design to get “close enough” with just plain requests.&lt;/p&gt;

&lt;p&gt;I want to highlight how you &lt;em&gt;can&lt;/em&gt; design good HTTP fallbacks, even in a complex set of behaviors.&lt;/p&gt;

&lt;p&gt;To reiterate the flows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;People can post directly to a timeline when viewing it, and they should be able to quickly Post Through It.&lt;/li&gt;
  &lt;li&gt;They can also &lt;em&gt;view&lt;/em&gt; a post-seeing metadata and connections for it.&lt;/li&gt;
  &lt;li&gt;Finally, people can edit the post, either directly from the timeline or when viewing it via the permalink.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few steps above your standard CRUD responses! Especially when you think about how to get the user efficiently back to where they want to be. &lt;em&gt;But it is solvable, and Rails helps you solve it.&lt;/em&gt; Let’s look at the code:&lt;/p&gt;

&lt;details open=""&gt;
  &lt;summary&gt;Here's the Microposts Controller again, this time focused on show/edit/update and creation errors&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/organizations/microposts_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Organizations::MicropostsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Organizations&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseController&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CableReady&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Broadcaster&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;TimelineEdgeContextLoading&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;TimelineEntryCableCar&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;FlashMessagesCableCar&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="n"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'loaf.breadcrumbs.organization_micropost'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
               &lt;span class="n"&gt;organization_micropost_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@organization_micropost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cable_ready&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;render_micropost_show_in_container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;organization_micropost: &lt;/span&gt;&lt;span class="vi"&gt;@organization_micropost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cable_ready: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@creation_postable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Organization&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Micropost&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Postable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_from_sgid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;sgid: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:postable_sgid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;breadcrumb&lt;/span&gt; &lt;span class="ss"&gt;:new_organization_micropost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;postable_new_organization_micropost_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:postable_sgid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Organization&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MicropostForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;current_organization: &lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;current_user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit&lt;/span&gt;
    &lt;span class="n"&gt;breadcrumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'loaf.breadcrumbs.edit_organization_micropost'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
               &lt;span class="n"&gt;edit_organization_micropost_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@organization_micropost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Organization&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MicropostForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;micropost: &lt;/span&gt;&lt;span class="vi"&gt;@organization_micropost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;current_organization: &lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;current_user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cable_ready&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outer_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="s2"&gt;"[data-micropost-id='&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@organization_micropost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;']"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                 &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;form: &lt;/span&gt;&lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="vi"&gt;@timeline_edge_context&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                                 &lt;span class="ss"&gt;formats: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                                &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cable_ready: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordInvalid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ValidationError&lt;/span&gt;
    &lt;span class="n"&gt;default_respond_to_model_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;html_action: :new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Organization&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MicropostForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_micropost_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;micropost: &lt;/span&gt;&lt;span class="vi"&gt;@organization_micropost&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;

    &lt;span class="n"&gt;organization_micropost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost&lt;/span&gt;

    &lt;span class="n"&gt;updated_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'little_crm.microposts.updated_message'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:success&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flash_success_with_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;updated_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;icon: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost_icon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;organization_micropost_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;organization_micropost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cable_ready&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;render_flash_messages_to_cable_car_and_clear&lt;/span&gt;
        &lt;span class="n"&gt;render_micropost_cable_ready_update&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;cable_ready: &lt;/span&gt;&lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordInvalid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ActiveModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ValidationError&lt;/span&gt;
    &lt;span class="n"&gt;default_respond_to_model_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;html_action: :edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="vi"&gt;@form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_micropost_show_in_container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_micropost&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="s2"&gt;"[data-micropost-details-container]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;render_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"organizations/microposts/show/post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;organization_micropost: &lt;/span&gt;&lt;span class="n"&gt;organization_micropost&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                             &lt;span class="ss"&gt;formats: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                            &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_micropost_cable_ready_update&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
      &lt;span class="n"&gt;cable_car_replace_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="vi"&gt;@timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render_micropost_show_in_container&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;organization_micropost: &lt;/span&gt;&lt;span class="vi"&gt;@organization_micropost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;
&lt;/details&gt;

&lt;details open=""&gt;
  &lt;summary&gt;The Framework-level concern to handle errors in a form submission&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;PracticalFramework::Controllers::ErrorResponse&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_json_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="c1"&gt;# Renders a custom payload that client-side JS can handle.&lt;/span&gt;
      &lt;span class="c1"&gt;# Likely will get refactored to use CableCar, this is pre-CableCar code&lt;/span&gt;
      &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PracticalFramework&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FormBuilders&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_error_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;helpers: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :bad_request&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_html_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt;  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :bad_request&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;default_respond_to_model_validation_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_action&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;render_json_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;render_html_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="n"&gt;html_action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/details&gt;

&lt;details open=""&gt;
  &lt;summary&gt;The &lt;code&gt;TimelineEntryCableCar&lt;/code&gt; concern again&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/concerns/timeline_entry_cable_car.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TimelineEntryCableCar&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cable_car_replace_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;cable_car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outer_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="no"&gt;CableCarAndBroadcast&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;
&lt;/details&gt;

&lt;details open=""&gt;
  &lt;summary&gt;&lt;code&gt;CableCarAndBroadcast::Timeline&lt;/code&gt;, this time for replacing a micropost&lt;/summary&gt;

  &lt;div class="language-ruby highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/broadcasters/cable_car_and_broadcast/timeline.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CableCarAndBroadcast::Timeline&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;CableReady&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Broadcaster&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;helpers&lt;/span&gt;
    &lt;span class="no"&gt;ApplicationController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;helpers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace_timeline_entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;html_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CableCar&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TimelineEntryController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_timeline_entry_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;selector: &lt;/span&gt;&lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_gid_param&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="n"&gt;html_string&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;cableready_stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cable_ready&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;TimelineStreamChannel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cableready_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;outer_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cableready_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;organization_stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# app/controllers/cable_car/timeline_entry_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CableCar::TimelineEntryController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_timeline_entry_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"timeline_entry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;timeline_edge_context: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;formats: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:html&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;
&lt;/details&gt;

&lt;details open=""&gt;
  &lt;summary&gt;The micropost form&lt;/summary&gt;

  &lt;div class="language-erb highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;# app/views/organizations/microposts/_form.html.erb
&lt;span class="c"&gt;&amp;lt;%# locals: (form:, creation_postable: nil, timeline_edge_context: nil) -%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;input_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:content_text_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;application_form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;url_for&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;organization_id: &lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                           &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;reset_on_ajax_success: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;focus_on_ajax_success: &lt;/span&gt;&lt;span class="n"&gt;id_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
                          &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fieldset&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input_wrapper&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;autofocus: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_key?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:autofocus_micropost&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;#...) &lt;/span&gt;&lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cluster-compact justify-space-between"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;field_errors&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fallback_error_section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;options: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:generic_errors&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cluster-compact"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;PracticalFramework&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Components&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StandardButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"little_crm.microposts.form.submit_button_title"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="ss"&gt;html_options: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;disable: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt; &lt;span class="c1"&gt;#...&lt;/span&gt;
      &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class="c"&gt;&amp;lt;%# The timeline_edge_context is the record that's used to show the timeline, the timeline edges reference microposts, but
           are a separate set of models to separate their concerns -%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;micropost_cancel_edit_button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;formaction: &lt;/span&gt;&lt;span class="n"&gt;organization_timeline_edge_context_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;edge_context_gid: &lt;/span&gt;&lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_gid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;hidden_field_tag&lt;/span&gt; &lt;span class="ss"&gt;:edge_context_gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeline_edge_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_gid&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persisted?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;%# Show the "Nevermind" button when editing a micropost via its permalink %&amp;gt;&lt;/span&gt; 
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;micropost_cancel_edit_button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;formaction: &lt;/span&gt;&lt;span class="n"&gt;url_for&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;micropost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;organization_id: &lt;/span&gt;&lt;span class="n"&gt;current_organization&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;A big thing to aim for with your fail-safe fallback is to get something that &lt;strong&gt;mostly works and gets the person &lt;em&gt;near&lt;/em&gt; the right path.&lt;/strong&gt; It’s obviously not ideal, but it is there, and it works in an emergency.&lt;/p&gt;

&lt;p&gt;Another important note is how errors are handled. This is something I’ve chipped away at for &lt;strong&gt;months because&lt;/strong&gt; we’ve lost the institutional knowledge on how to best do this (and no one’s really codified it).&lt;/p&gt;

&lt;p&gt;The controller actions rescue any validation errors that are raised (I’m a huge proponent of using the &lt;code&gt;bang!&lt;/code&gt; methods for creation/saving/validation so the control flow is easier). These actions can respond to both:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A request that accepts JSON data. In this case, the errors are serialized into a format that the client-side JS can use to update the form’s validation state using the constraint validation APIs the browser provides and insert error messages.&lt;/li&gt;
  &lt;li&gt;A standard request that accepts HTML. In this case, we re-render the form that currently has the errors, which then renders the errors as part of its markup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note that the HTML fallback does not need to be perfect. We just need to make sure the user isn’t unnecessarily blocked&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let’s look at what happens if there is an error when someone is submitting a post on the organization’s timeline.&lt;/p&gt;

&lt;p&gt;When JS is working, the errors are rendered inline. When JS is not working, the browser is redirected to a &lt;strong&gt;basic fallback page that renders the form&lt;/strong&gt;.&lt;/p&gt;

&lt;figure&gt;
  &lt;video controls="" width="480" height="300" poster="/assets/networking-stack/CableCar inline errors and submit Thumbnail.png"&gt;
    &lt;source src="/assets/networking-stack/CableCar inline errors and submit.mp4" type="video/mp4" /&gt;
  &lt;/video&gt;
  &lt;figcaption&gt;
  &lt;p&gt;Demos showing off how the microposts form works even when JS is disabled&lt;/p&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Again, this is not the ideal experience. But it &lt;em&gt;is&lt;/em&gt; an experience that allows the person to move forward, do what they need to do.&lt;/p&gt;

&lt;p&gt;Admittedly, there are some cases where you do need JS. A concrete example is Stripe’s Payment Elements. In my mind, in those cases, progressive enhancement is rendering an error message directly on the page— something like:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Sorry! We ran into an issue rendering the payment element here. Please try reloading the page, and contact support if you still run into issues!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s progressive enhancement because the user knows what happened, what’s going on, and how to resolve it. And it’s infinitely better than a page that’s mysteriously busted or a form that won’t submit and shows no error messages.&lt;/p&gt;

&lt;p&gt;You can imagine the way I buried my head in my hands when I demoed this to an acquaintance, and their shitpost response was:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“I didn’t realize you could make usable websites without Javascript”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(Side note: this was the impetus for one of my best shitposts, IMO)&lt;/p&gt;

&lt;iframe src="https://ruby.social/@tcannonfodder/112854613138758394/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"&gt;&lt;/iframe&gt;
&lt;script src="https://ruby.social/embed.js" async="async"&gt;&lt;/script&gt;

&lt;h2 id="ujs-is-still-a-fantastic-design-and-you-should-be-using-mrujs"&gt;UJS is still a fantastic design, and you should be using Mrujs&lt;/h2&gt;

&lt;p&gt;A quick history primer: UJS stands for “Unobtrusive Javascript”, and &lt;a href="https://en.wikipedia.org/wiki/Unobtrusive_JavaScript"&gt;Wikipedia has a great summary &amp;amp; overall page&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Unobtrusive JavaScript is a general approach to the use of client-side JavaScript in web pages so that if JavaScript features are partially or fully absent in a user’s web browser, then the user notices as little as possible any lack of the web page’s JavaScript functionality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rails 6 shipped with a library called, shockingly, UJS. Its design was relatively small, but immensely powerful and flexible. By using &lt;code&gt;data&lt;/code&gt; attributes &amp;amp; event handlers, you could augment your markup to perform actions if Javascript was enabled. Basic stuff that every web app wants, such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Submitting forms via AJAX.&lt;/li&gt;
  &lt;li&gt;Presenting confirmation dialogs.&lt;/li&gt;
  &lt;li&gt;Enabling/disabling elements (and changing the inner content if needed).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While UJS unceremoniously got dumped by DHH, Konnor Rogers has written a modern version, &lt;a href="https://mrujs.netlify.app"&gt;Mrujs&lt;/a&gt;, and you should absolutely be using it. &lt;strong&gt;The UJS design is evergreen because it’s lightweight and leans into the browser’s strengths&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;UJS, largely speaking, is focused on augmentation. It operates on the assumption that Javascript will not be available, and &lt;strong&gt;that you might not need it in the first place&lt;/strong&gt;. Which is becoming increasingly true! Look at all the stuff we’ve gotten in Baseline! Unlike Turbo/Hotwire/React/etc., UJS is designed to take up as little space as possible, defer to the browser, and automatically have its foot out the exit door when it knows it’s not needed. &lt;strong&gt;Why would you waste bandwidth on a framework&lt;/strong&gt; &lt;strong&gt;that browsers don’t need?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The other browser strength that UJS rests on is heavy usage of event dispatching. &lt;strong&gt;The browser is a machine that spits out events and can handle those events; it’s really good at it, too!&lt;/strong&gt; The original UJS library wasn’t as good as I’d like about its event handling (or even documenting it), but Mrujs is.&lt;/p&gt;

&lt;details open=""&gt;
  &lt;summary&gt;Mrujs event handlers to reset and autofocus a form&lt;/summary&gt;

  &lt;div class="language-js highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resetOnAjaxSuccessEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;resetOnAjaxSuccess&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;focusOnAjaxSuccessEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focusTargetSelector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;focusOnAjaxSuccess&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;focusTargetSelector&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focusTarget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;focusTargetSelector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;focusTarget&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resetOnAjaxSuccessEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;practical-framework&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;focusOnAjaxSuccessEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;practical-framework&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`ajax:success`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resetOnAjaxSuccessEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`ajax:success`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;focusOnAjaxSuccessEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/details&gt;

&lt;h2 id="other-bits--bobs"&gt;Other bits &amp;amp; bobs&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cableready.stimulusreflex.com/hello-world/"&gt;CableReady’s documentation is pretty comprehensive.&lt;/a&gt; Certainly more comprehensive than the docs for most new Rails features since Rails 5. I don’t necessarily like the tone, but I’ll take memeified docs over no docs any day of the week.&lt;/p&gt;

&lt;p&gt;One argument I’ve heard against CableCar is that sometimes the API doesn’t feel very Rails-like. I haven’t found that to be a significant deterrent, and that could be solved with refactoring/helpers/magic. Also, there are a few counterarguments:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;One ethos of Rails is to use sensible, easily understandable behavior in 90% of cases. CableCar is a protocol that does that.&lt;/li&gt;
  &lt;li&gt;Rails also advocates for “using boring technology/patterns.” CableReady/CableCar are &lt;strong&gt;infinitely&lt;/strong&gt; more boring than Turbo/Hotwire. 👀&lt;/li&gt;
  &lt;li&gt;Browsers are complex systems, and using their universal mechanisms allows more developers to work on Rails apps and encourages the growth of the ecosystem. Much more than a bespoke frontend framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id="musing-how-much-of-an-app-really-needs-to-be-real-time"&gt;Musing: How much of an app really needs to be real-time?&lt;/h2&gt;

&lt;p&gt;As I was building this out, I conducted a very unscientific poll. The results aren’t even close to being statistically significant, but I still think they’re interesting!&lt;/p&gt;

&lt;div style="display: flex; max-width: 80vw; flex-wrap: wrap; justify-content: center; gap: 3ch;"&gt;

  &lt;iframe src="https://ruby.social/@tcannonfodder/112861280550087682/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"&gt;&lt;/iframe&gt;&lt;script src="https://ruby.social/embed.js" async="async"&gt;&lt;/script&gt;
  &lt;iframe src="https://ruby.social/@tcannonfodder/112866134781360484/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"&gt;&lt;/iframe&gt;&lt;script src="https://ruby.social/embed.js" async="async"&gt;&lt;/script&gt;
&lt;/div&gt;

&lt;p&gt;From these results, my experience working &amp;amp; maintaining apps, and hearing what folks struggle with in their apps; the vibe is that going real-time-first only happens in demos &amp;amp; simple code.&lt;/p&gt;

&lt;p&gt;I also asked &lt;a href="https://www.konnorrogers.com"&gt;Konnor Rogers&lt;/a&gt;, who has extensive frontend knowledge and is part of the Web Awesome team! And he agreed with my suspicions:&lt;/p&gt;

&lt;div style="display: flex; max-width: 80vw; flex-wrap: wrap; flex-direction: column; align-content: center;"&gt;
  &lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;🌶️ the more I think about morphing, the more I think it&amp;#39;s the wrong tool for the job.&lt;br /&gt;&lt;br /&gt;It&amp;#39;s easy to end up with stale nodes, nodes that have event listeners they shouldn&amp;#39;t, unless your server is recording every possible client interaction, it&amp;#39;ll still wipe away client state. &lt;a href="https://t.co/tJbuxLpkmb"&gt;https://t.co/tJbuxLpkmb&lt;/a&gt;&lt;/p&gt;&amp;mdash; Konnor Rogers (@RogersKonnor) &lt;a href="https://twitter.com/RogersKonnor/status/1820527741441352116?ref_src=twsrc%5Etfw"&gt;August 5, 2024&lt;/a&gt;&lt;/blockquote&gt; &lt;script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

  &lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;I&amp;#39;ve written like 3 responses to this, but char limit is screwing me.&lt;br /&gt;&lt;br /&gt;morphing / preserving nodes has a place. Implicitly doing so is a super easy footgun that&amp;#39;s hard to debug. View transitions should help solve this problem in the future. Explicit marking makes debugging easier&lt;/p&gt;&amp;mdash; Konnor Rogers (@RogersKonnor) &lt;a href="https://twitter.com/RogersKonnor/status/1820543501937065990?ref_src=twsrc%5Etfw"&gt;August 5, 2024&lt;/a&gt;&lt;/blockquote&gt; &lt;script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

  &lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;There&amp;#39;s the other problem of it&amp;#39;s impossible to stream HTML with morphing. (Well not impossible, but way more error prone, and behavior is bizarre)&lt;br /&gt;&lt;br /&gt;Turbo also requires full HTML responses before it can render a new page, leading to additional &amp;quot;slowness&amp;quot; if not cached&lt;/p&gt;&amp;mdash; Konnor Rogers (@RogersKonnor) &lt;a href="https://twitter.com/RogersKonnor/status/1820543796557619315?ref_src=twsrc%5Etfw"&gt;August 5, 2024&lt;/a&gt;&lt;/blockquote&gt; &lt;script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

  &lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;&lt;a href="https://twitter.com/jaffathecake?ref_src=twsrc%5Etfw"&gt;@jaffathecake&lt;/a&gt; has a great article from *checks notes* 8 years ago which is about Turbolinks, but still applies to Turbo.&lt;br /&gt;&lt;br /&gt;By needing the full response before you can render, you can actually make content slower particularly on unreliable networks.&lt;a href="https://t.co/k9wA6xkUAf"&gt;https://t.co/k9wA6xkUAf&lt;/a&gt;&lt;/p&gt;&amp;mdash; Konnor Rogers (@RogersKonnor) &lt;a href="https://twitter.com/RogersKonnor/status/1820544147687915595?ref_src=twsrc%5Etfw"&gt;August 5, 2024&lt;/a&gt;&lt;/blockquote&gt; &lt;script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

  &lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Yes. It&amp;#39;s a powerful tool with sharp edges, little documentation on the inner workings, and don&amp;#39;t get me started on the meta tags. They feel like someone set some fucking tanagrams in front of me and was like &amp;quot;figure out the correct combination to fit your use case&amp;quot;&lt;/p&gt;&amp;mdash; Konnor Rogers (@RogersKonnor) &lt;a href="https://twitter.com/RogersKonnor/status/1820546702022979611?ref_src=twsrc%5Etfw"&gt;August 5, 2024&lt;/a&gt;&lt;/blockquote&gt; &lt;script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;

&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;my thoughts are very few features need realtime / reactive.&lt;/p&gt;

  &lt;p&gt;HTTP requests / responses cycles are well understood. Have great caching, work well with load balancing etc.&lt;/p&gt;

  &lt;p&gt;Realtime / reactive has an entirely different set of problems. You probably want optimistic updates, which means some kind of CRDT backing mechanism. Youll probably want local first which usually means a heavy client.&lt;/p&gt;

  &lt;p&gt;And this doesnt even touch on auth / accessibility. Realtime / reactive is hard to get right.&lt;/p&gt;

  &lt;p&gt;[…]&lt;/p&gt;

  &lt;p&gt;FWIW, Turbo is like 99% request / response. So little of turbo is real time / reactive. It’s almost entirely HTTP and like a tiny bit of web sockets for model broadcasts&lt;/p&gt;

  &lt;p&gt;[…]&lt;/p&gt;

  &lt;p&gt;where I think Turbo went wrong is the following:&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;Links should only be able to make &lt;code&gt;GET&lt;/code&gt; requests&lt;/li&gt;
    &lt;li&gt;&lt;code&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; should be a &lt;code&gt;&amp;lt;turbo-action&amp;gt;&lt;/code&gt;, stream is just too confusing. It makes people think they’re using a web socket when it’s really just a DOM operation&lt;/li&gt;
    &lt;li&gt;Turbo frames are clunky with managing ids. Scoped navigation as the default is equally weird.&lt;/li&gt;
    &lt;li&gt;There’s like 40 different combo of meta tags. It’s so hard to keep them all straight.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hearing someone else with a lot more experience mirror my thoughts helps me feel confident in my claims in this post.&lt;/p&gt;

&lt;p&gt;Thinking in terms of complexity &amp;amp; the mismatch between technical and user expectations; users might not actually need &lt;strong&gt;truly&lt;/strong&gt; real-time updates. &lt;a href="https://www.youtube.com/watch?v=2Z2s4mRxT0A"&gt;At Blue Ridge Ruby, Brian Childress gave a great talk about managing complexity&lt;/a&gt;. And the biggest takeaway I got from that is how “real-time updates” actually means “8 AM, once a day” for clients in practice. I highly recommend watching it!&lt;/p&gt;

&lt;p&gt;Given all of this; I do believe that the best approach for the majority of apps is:&lt;/p&gt;

&lt;p&gt;Write progressively enhanced request/responses from the ground up, with the &lt;em&gt;jackpot&lt;/em&gt; &lt;em&gt;ideal&lt;/em&gt; being Websocket updates and the general assumption that your JS will be broken or not fully loaded.&lt;/p&gt;

&lt;p&gt;Which is what you get with smart controller design &amp;amp; abstractions, UJS/CableCar, and a sprinkling of CableReady when it matters.&lt;/p&gt;

&lt;h2 id="weve-wasted-a-lot-of-time-reinventing-solved-problemsarguably-worse"&gt;We’ve wasted a lot of time reinventing solved problems—arguably worse!&lt;/h2&gt;

&lt;p&gt;I don’t want to belabor this point too long because, frankly, it’s exhausting, demoralizing, and people with more knowledge of the internal politics than I do have already covered this.&lt;/p&gt;

&lt;p&gt;UJS is such a lightweight tool, which is the direction that’s best as browsers do more and more. It slots in nicely with other browser APIs, like Web Components, and even heavier frontend frameworks. Unfortunately, it got dumped unceremoniously for a stack that’s significantly more complex. We were too quick to throw out UJS, which still has some excellent design decisions, and CableCar’s protocol works better than Turbo Streams for updates that can be progressively enhanced.&lt;/p&gt;

&lt;p&gt;Turbo and Hotwire were latecomers. CableReady shipped years before them, and as I’ve (hopefully) shown, it solved the same problems better. So much so that Turbo/Hotwire 8 has enlisted some CableReady maintainers to get to feature parity with CableReady.&lt;/p&gt;

&lt;p&gt;Even if they can get there (I doubt it; the CableReady approach is much more robust), the question is: &lt;strong&gt;why the hell are we doing that?&lt;/strong&gt; Why don’t we just use the better framework, even if it didn’t &lt;em&gt;originate&lt;/em&gt; from the Rails’ core? Why not have Rails ship with it by default, or hell &lt;em&gt;contribute&lt;/em&gt; to it? DHH has no problem jettisoning frontend approaches between major versions (he’s done so multiple times!). There’s precedent for it.&lt;/p&gt;

&lt;p&gt;The best thing to happen to Rails was when Merb renamed itself to Rails &amp;amp; gave developers a proper plugin architecture. You aren’t chained to Rails’ defaults, and we can communally choose the better tools without having Rails “bless it.” Because even if they do bless a frontend framework, that’s no guarantee for success, maintenance, or growth. So I’d argue we should choose the better tool. &lt;strong&gt;Community consensus is also a form of standardization.&lt;/strong&gt;&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:thomascannon.me,2022-10-11:/guides/rails-development-and-passkeys.html</id>
    <title type="html">Getting your Rails dev environment ready for passkeys</title>
    <published>2022-10-11T00:00:00Z</published>
    <updated>2022-10-11T00:00:00Z</updated>
    <link rel="alternate" href="https://thomascannon.me/guides/rails-development-and-passkeys.html" type="text/html"/>
    <content type="html">&lt;h1 id="getting-your-rails-dev-environment-ready-for-passkeys"&gt;Getting your Rails dev environment ready for passkeys&lt;/h1&gt;

&lt;p&gt;I spent the whole day digging through WebKit bug reports &amp;amp; wrangling with &lt;code&gt;puma-dev&lt;/code&gt; , but finally have a working solution.&lt;/p&gt;

&lt;p&gt;The gist is: while Chrome, Firefox, and &lt;em&gt;non-macOS implementations of WebKit&lt;/em&gt; support &lt;code&gt;*.localhost&lt;/code&gt; subdomains (and register them as a Secure Context, see: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure"&gt;This MDN article&lt;/a&gt; ), WebKit on macOS does not, because of underlying macOS networking APIs. There’s a whole set of bugs about it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://bugs.webkit.org/show_bug.cgi?id=160504"&gt;https://bugs.webkit.org/show_bug.cgi?id=160504&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://bugs.webkit.org/show_bug.cgi?id=171934#c102"&gt;https://bugs.webkit.org/show_bug.cgi?id=171934#c102&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So your Rails development environment needs to run with HTTPS, which requires a certificate, which is a whole mess.&lt;/p&gt;

&lt;h2 id="puma-dev-to-the-rescue"&gt;&lt;code&gt;puma-dev&lt;/code&gt; to the Rescue&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;puma-dev&lt;/code&gt; solves the HTTPS issue elegantly, but I’ve never liked how it runs as a daemon process by default. macOS does not have good daemon support; and for execution environments as volatile as development, daemons aren’t the right answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update: Arrrgf; turns out that &lt;code&gt;puma-dev&lt;/code&gt; only listens to &lt;code&gt;127.0.0.1&lt;/code&gt; when run in the foreground, so it has to run as a daemon if you need cross-device testing (such as through VMs or on mobile devices).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, I &lt;em&gt;really&lt;/em&gt; don’t want to lose the ability to quickly kill &amp;amp; reboot my development environment. Again, the problem of macOS not having good daemon support and development being too volatile of an execution environment for daemons to be practical. So I ended up writing a script that:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Force stops the &lt;code&gt;puma-dev&lt;/code&gt; daemon&lt;/li&gt;
  &lt;li&gt;Prints out some text to help with attaching the debugger&lt;/li&gt;
  &lt;li&gt;Starts tailing puma’s daemon logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="language-sh highlighter-rouge"&gt;&lt;div class="highlight"&gt;&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"make sure to attach a debugger with (or alias): rdbg -A"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"force-stopping puma-dev"&lt;/span&gt;
puma-dev &lt;span class="nt"&gt;-stop&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"watching puma with: tail -f ~/Library/Logs/puma-dev.log"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"============"&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/Library/Logs/puma-dev.log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The ideal solution would be for &lt;code&gt;puma-dev&lt;/code&gt; to add a flag to specify what hosts to listen on. There’s a PR for some of this, but a customizable command-line flag (like for ports) would be a great solution; since it allows for flexibility: &lt;a href="https://github.com/puma/puma-dev/issues/306"&gt;https://github.com/puma/puma-dev/issues/306&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In case it’s useful for anyone, I’ve kept my original version at the end of this post. It’s a good example of how you can hook in Ruby’s built-in debugger as part of a larger buildchain.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;code&gt;puma-dev&lt;/code&gt; has the option to run in a foreground mode. But since &lt;code&gt;puma-dev&lt;/code&gt; doesn’t support TTY for its child processes, you lose the ability to use the built-in Ruby debugger.&lt;/p&gt;

&lt;p&gt;Thankfully, Ruby’s built-in debugger has a special wrapper command that we can use to automatically get an &lt;strong&gt;optional&lt;/strong&gt; debugger for any process spawned by &lt;code&gt;puma-dev&lt;/code&gt; .&lt;/p&gt;

&lt;h2 id="the-commands"&gt;The commands&lt;/h2&gt;
&lt;p&gt;The command to run &lt;code&gt;puma-dev&lt;/code&gt; in the foreground with the debugger hooks is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;
rdbg --nonstop -O -c -- bundle exec puma-dev
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To attach the debugger process, run:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;
rdbg -A
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Which is…cumbersome. I prefer using an alias:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;
alias attachdebugger='rdbg -A'
&lt;/code&gt;&lt;/p&gt;

</content>
  </entry>
  <entry>
    <id>tag:thomascannon.me,2021-10-31:/posts/2022-10-11-rails-dev-environment-and-passkeys.html</id>
    <title type="html">Setup guide to prepare for passkeys in your Rails development environment</title>
    <published>2021-10-31T00:00:00Z</published>
    <updated>2021-10-31T00:00:00Z</updated>
    <link rel="alternate" href="https://thomascannon.me/posts/2022-10-11-rails-dev-environment-and-passkeys.html" type="text/html"/>
    <content type="html">&lt;p&gt;I spent 4 hours debugging how to sidestep bugs with macOS’s DNS resolver, and running &lt;code&gt;puma-dev&lt;/code&gt; in the foreground with streamlined &lt;code&gt;ruby-debug&lt;/code&gt; support. The &lt;a href="/guides/rails-development-and-passkeys.html"&gt;setup guide’s here&lt;/a&gt;, save yourself the agony&lt;/p&gt;
</content>
  </entry>
  <entry>
    <id>tag:thomascannon.me,2021-10-31:/posts/2021-10-31-revamped-noko-dialogs-guide.html</id>
    <title type="html">Writeup about Noko's new dialogs framework</title>
    <published>2021-10-31T00:00:00Z</published>
    <updated>2021-10-31T00:00:00Z</updated>
    <link rel="alternate" href="https://thomascannon.me/posts/2021-10-31-revamped-noko-dialogs-guide.html" type="text/html"/>
    <content type="html">&lt;p&gt;I just published a high-level guide on a &lt;a href="/guides/rebuilding-nokos-dialogs.html"&gt;How we rebuilt Noko’s Dialogs&lt;/a&gt;, migrating from an ad-hoc series of tools to a structured framework. It was an interesting challenge, applying R&amp;amp;D I’d been working on for the past 3 years.&lt;/p&gt;

&lt;p&gt;Let me know what you think!&lt;/p&gt;
</content>
  </entry>
</feed>
