<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: matt swanson</title>
    <description>The latest articles on Forem by matt swanson (@swanson).</description>
    <link>https://forem.com/swanson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F365814%2F88a29472-0223-458a-a580-fe97259c9470.jpg</url>
      <title>Forem: matt swanson</title>
      <link>https://forem.com/swanson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/swanson"/>
    <language>en</language>
    <item>
      <title>Event sourcing for smooth brains: building a basic event-driven system in Rails</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Sun, 21 Jul 2024 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/event-sourcing-for-smooth-brains-building-a-basic-event-driven-system-in-rails-2bjl</link>
      <guid>https://forem.com/swanson/event-sourcing-for-smooth-brains-building-a-basic-event-driven-system-in-rails-2bjl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Event sourcing&lt;/strong&gt; is a jargon filled mess that is unapproachable to many developers, often using five dollar words like “aggregate root” and “projections” to describe basic concepts.&lt;/p&gt;

&lt;p&gt;While the high standards of “full event sourcing” might recommend building your entire application around the concept, it is often a good idea to start with a smaller, more focused area of your codebase.&lt;/p&gt;

&lt;p&gt;I was familiar with the broadest strokes of event sourcing, but it always felt way overkill for me and something that involved a bunch of Java code and Kafka streams and all of the pain that comes with distributed, eventually consistent systems.&lt;/p&gt;

&lt;p&gt;But lately I have been building with a very basic, dumbed down version of event sourcing (I call this “event sourcing for smooth brains”) and I can see how aspects of this model can be a great fit for a boring Rails monolith.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did I go down this path?
&lt;/h2&gt;

&lt;p&gt;Your application is generating tons of events. Even if you don’t think about them as events, they are there. Imagine an Issue in a GitHub project: a new issue is created, a comment is added, a label is added, and so on.&lt;/p&gt;

&lt;p&gt;It’s common to need to list these events in some kind of feed. And as the application becomes more complex, you’ll find that you need to do more and more “things” when an event happens.&lt;/p&gt;

&lt;p&gt;Think back to the GitHub issue example: when a comment is added, you might need to email the person who created the issue, send a notification to a team member, update a counter, trigger an automated action, run a spam check, update the commenter’s contribution graph.&lt;/p&gt;

&lt;p&gt;Pretty quickly you’ll be writing a bunch of code to handle all of these different things and having some standard patterns for interacting with events is going to be required.&lt;/p&gt;

&lt;p&gt;I’ll describe the simple version we’ve been using for a while now at &lt;a href="https://arrows.to/" rel="noopener noreferrer"&gt;Arrows&lt;/a&gt;. We are not operating at the scale of GitHub or any other large application, but it has served us well and the patterns are simple enough that we can scale for a long time before we need to add more complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s about the events
&lt;/h2&gt;

&lt;p&gt;Instead of worrying about &lt;em&gt;projections&lt;/em&gt;, &lt;em&gt;aggregates&lt;/em&gt;, &lt;em&gt;reactors&lt;/em&gt;, &lt;em&gt;command query responsibility separation&lt;/em&gt;, and &lt;em&gt;read models&lt;/em&gt; we’re just going to focus on the events.&lt;/p&gt;

&lt;p&gt;We’re also going to focus on events around one specific domain: issues.&lt;/p&gt;

&lt;p&gt;Create an &lt;code&gt;issues_events&lt;/code&gt; table with this schema (adjust to your liking, but this is the basic structure I use):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:issues_events&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;t&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="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:issue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;to_table: :users&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="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;index: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &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;null: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonb&lt;/span&gt; &lt;span class="ss"&gt;:extra&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="s2"&gt;"{}"&lt;/span&gt;

  &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="ss"&gt;:occurred_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"CURRENT_TIMESTAMP"&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="nf"&gt;timestamps&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a Rails app, I like making the event model under the &lt;code&gt;Issues&lt;/code&gt; namespace, especially when you are using such a common name as “event”.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Issues&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:project&lt;/span&gt;

  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s2"&gt;"Issues::Event"&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;Issues::Event&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:issue&lt;/span&gt;

  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The event model
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Issue::Event&lt;/code&gt; model is a simple model that stores the event data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;action&lt;/code&gt;: the name of the event (e.g. “comment_added”, “label_added”, etc). We put some validations on this to make sure we don’t have any typos or invalid events.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;actor&lt;/code&gt;: the user that performed the action. We also have a “system” user for events that are generated by the application itself and not by a specific person&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;occurred_at&lt;/code&gt;: the time the event occurred&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;record&lt;/code&gt;: an optional polymorphic association to the record that was acted on (e.g. a &lt;code&gt;Comment&lt;/code&gt; or a &lt;code&gt;Label&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extra&lt;/code&gt;: a JSONB column for storing any extra data that might be needed. Generally be a bit weary of this because it will be unstructured, but for basic things it’s fine
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Issues::Event&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:issue&lt;/span&gt;

  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;polymorphic: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="no"&gt;SUPPORTED_ACTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[
    comment_added
    comment_deleted
    comment_viewed
    label_added
    ...
  ]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;inclusion: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;in: &lt;/span&gt;&lt;span class="no"&gt;SUPPORTED_ACTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"%{value} is not a valid action"&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;p&gt;Nothing fancy here, just a basic Rails model that you can query and interact with just like any other model.&lt;/p&gt;

&lt;p&gt;Now you’ll want a nice API to create these events. One thing we quickly found in practice is that some events would need to be throttled.&lt;/p&gt;

&lt;p&gt;For example, if you want to track that a comment was viewed, you don’t necessarily need to record every single page view. You could group up the events within a certain time period into a single “comment viewed” event.&lt;/p&gt;

&lt;p&gt;In our app, we wanted to be able to record events around lack of activity (e.g. this issue has not been viewed in a while) using a cron job but we didn’t want to keep adding &lt;code&gt;no_activity&lt;/code&gt; events every time we checked so we set the throttle to be greater than the polling interval.&lt;/p&gt;

&lt;p&gt;In “proper” event sourcing, you might record each of those events, then roll them up or create an intermediate snapshot or something fancier. For us, it was simple enough to do the throttling at creation time. We lose the full, unabridged history, but we don’t need to build other mechanisms to handle this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Issue&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;occurred_at: :desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s2"&gt;"Issues::Event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;dependent: :destroy&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_event!&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="ss"&gt;actor: &lt;/span&gt;&lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;record: &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;extra: &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="ss"&gt;throttle_within: &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;if&lt;/span&gt; &lt;span class="n"&gt;throttle_within&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;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;record: &lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;actor: &lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;occurred_at: &lt;/span&gt;&lt;span class="n"&gt;throttle_within&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;&lt;span class="o"&gt;..&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;existing&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&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;touch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:occurred_at&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;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;record: &lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;actor: &lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;extra: &lt;/span&gt;&lt;span class="n"&gt;extra&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;# Recording events&lt;/span&gt;
&lt;span class="vi"&gt;@issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_event!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:comment_added&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;actor: &lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;record: &lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Throttling events&lt;/span&gt;
&lt;span class="vi"&gt;@issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_event!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:comment_viewed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;record: &lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;throttle_within: &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Adding some extra data for extra bits of metadata&lt;/span&gt;
&lt;span class="vi"&gt;@issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:label_added&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;extras: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="vi"&gt;@label&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add a &lt;code&gt;record_event!&lt;/code&gt; method to the &lt;code&gt;Issue&lt;/code&gt; model that will create the event and optionally throttle it if it is within a certain time period.&lt;/p&gt;

&lt;p&gt;To throttle, we look up an existing event for the same action, action, and record that occurred within the throttle time period and touch it to update the &lt;code&gt;occurred_at&lt;/code&gt; timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Voila an activity feed!
&lt;/h2&gt;

&lt;p&gt;So far, this is neat and all…but all we’ve done is create a glorified activity feed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Issues::FeedsController&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="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@issue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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;:issue_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="vi"&gt;@page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pagy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;occurred_at: :desc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;items: &lt;/span&gt;&lt;span class="mi"&gt;10&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;# Create a view or component to render each event in the feed&lt;/span&gt;
&lt;span class="c1"&gt;# For each item you have `action`, `actor`, `occurred_at`, and `record`&lt;/span&gt;
&lt;span class="c1"&gt;# to construct a line item. You can define icons for each type, different&lt;/span&gt;
&lt;span class="c1"&gt;# colors, etc (exercise left to the reader)&lt;/span&gt;
&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;Issues&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Feed&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="vi"&gt;@events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now this is useful to have and will be easy to add more events to over time for sure. But it’s not really showing the power of event sourcing.&lt;/p&gt;

&lt;p&gt;For that, we need to actually do other stuff with the events.&lt;/p&gt;

&lt;p&gt;In my work at &lt;a href="https://arrows.to/" rel="noopener noreferrer"&gt;Arrows&lt;/a&gt; (and nearly every other Rails app I’ve worked on), you eventually will build up several different integrations, notification systems, and light “metric” dashboards that need to know when things happen in the app.&lt;/p&gt;

&lt;p&gt;In the case of our &lt;code&gt;Issue&lt;/code&gt; model, let’s say that when a comment is added, I need to send an email to the Issue creator, add it to the Issue creators GitHub notification inbox, and post it to a Slack channel.&lt;/p&gt;

&lt;p&gt;Instead of reaching for heavier approaches like an Event Bus or a Pub/Sub library, we can use Rails &lt;code&gt;after_create_commit&lt;/code&gt; callbacks to do this.&lt;/p&gt;

&lt;p&gt;Gasp! A callback! Aren’t those evil? Well, no. You can certainly make a mess but callbacks are one of the powerful tools in Rails. It’s a sharp knife, which means “be careful with it”, not “ban it from the kitchen”.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Issues::Event&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="n"&gt;after_create_commit&lt;/span&gt; &lt;span class="ss"&gt;:broadcast&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;broadcast&lt;/span&gt;
    &lt;span class="no"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Inbox&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;process_later&lt;/span&gt;
    &lt;span class="no"&gt;AppNotification&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Inbox&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;process_later&lt;/span&gt;
    &lt;span class="no"&gt;Slack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Inbox&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="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;process_later&lt;/span&gt;
    &lt;span class="c1"&gt;# Add whatever makes sense for your app&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;p&gt;The &lt;code&gt;broadcast&lt;/code&gt; method is called after the event is created (note: it will be called the first time when an event is throttled, but not after that…this behavior may not be appropriate for all use cases).&lt;/p&gt;

&lt;p&gt;We then send that event into a bunch of different objects that I like to call “inboxes”. Each inbox can determine: if the event should be sent, what data to send, and how to send it. By using the familiar Rails &lt;code&gt;_later&lt;/code&gt; suffix, we hint that these should almost certainly be run as background jobs.&lt;/p&gt;

&lt;p&gt;I won’t show the code for each inbox, but the general structure is something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Email::Inbox&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;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&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;process_later&lt;/span&gt;
    &lt;span class="no"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@event&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;process&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="vi"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&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;:comment_added&lt;/span&gt;
      &lt;span class="c1"&gt;# Send an email to the issue creator&lt;/span&gt;
      &lt;span class="c1"&gt;# Send an email to any people subscribed to the issue&lt;/span&gt;
      &lt;span class="c1"&gt;# Send an email to any project maintainers with notifications enabled&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:label_added&lt;/span&gt;
      &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="ss"&gt;:comment_viewed&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;end&lt;/span&gt;

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

  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Job&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;process&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;p&gt;As you can imagine, some inboxes will handle a lot of different event types and some will only handle a few. But the general pattern is the same: create a class that receives the event and then processes it.&lt;/p&gt;

&lt;p&gt;You can structure the inboxes however you want, including extracting classes to handle the events as the logic grows. This is especially nice for inboxes like a Slack integration where we can make objects like &lt;code&gt;Slack::MessageBuilder&lt;/code&gt; that can handle converting an event object into the formatted API payloads that Slack expects.&lt;/p&gt;

&lt;p&gt;As you add more functionality to your application, you’ll find that you have a clear and easy place to put the code to handle what to do when an event happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reaping what you’ve sown
&lt;/h2&gt;

&lt;p&gt;Now that you have the basics setup, features that seemed super complicated can become much more straightforward to build.&lt;/p&gt;

&lt;p&gt;If you want to build a new integration to an external API, you have the seams to put it into place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Linear::Inbox&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;process&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synced_to_linear?&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="vi"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&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;:comment_added&lt;/span&gt;
      &lt;span class="no"&gt;Linear&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_comment!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&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;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;p&gt;If you want to build a basic workflow automation system, you have a great start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Issues::Workflow&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:issue&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:conditions&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:actions&lt;/span&gt;

  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:triggered_on&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:triggered_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;inclusion: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;in: &lt;/span&gt;&lt;span class="no"&gt;Issues&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUPPORTED_ACTIONS&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="vi"&gt;@issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;workflows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;triggered_on: &lt;/span&gt;&lt;span class="s2"&gt;"comment_added"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;conditions: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;attribute: &lt;/span&gt;&lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;operator: &lt;/span&gt;&lt;span class="s2"&gt;"lt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="s2"&gt;"2022-01-01"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ss"&gt;actions: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"This issue is stale, open a new one"&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;p&gt;If you want the ability to do basic “history” queries to see how often a feature is used, you’ve got a solid foundation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Issue&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"comment_deleted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;issue: &lt;/span&gt;&lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need to “replay” events to backfill data, you can query the events like normal ActiveRecord models and do your own processing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;last_commented_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;events&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"comment_added"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:occurred_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="vi"&gt;@issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;last_commented_at: &lt;/span&gt;&lt;span class="n"&gt;last_commented_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern has been powerful for us at &lt;a href="https://arrows.to/" rel="noopener noreferrer"&gt;Arrows&lt;/a&gt;. We’ve been able to quickly build out several “systems” with a small team. Our main domain object has around 50 different event types and we’ve found it very easy to work with over time.&lt;/p&gt;

&lt;p&gt;Adding new features is a breeze and the code is easy to maintain. Because the event creation and processing are decoupled, it’s easy to test and we feel safe that we won’t breaking existing behaviors.&lt;/p&gt;

&lt;p&gt;And lastly, because it is basic, simple code (instead of a full-blown event sourcing library or a bunch of extra services), it’s easy to understand and we actually use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acknowledgements and further reading
&lt;/h2&gt;

&lt;p&gt;The idea of event sourcing came back onto my radar after hearing about it from &lt;a href="https://x.com/DCoulbourne" rel="noopener noreferrer"&gt;Daniel Coulbourne&lt;/a&gt; and &lt;a href="https://cmorrell.com/" rel="noopener noreferrer"&gt;Chris Morrell&lt;/a&gt;in the context of their Laravel package &lt;a href="https://verbs.thunk.dev/" rel="noopener noreferrer"&gt;Verbs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The always excellent Martin Fowler blog has a nice post on &lt;a href="https://martinfowler.com/eaaDev/EventSourcing.html" rel="noopener noreferrer"&gt;Event Sourcing&lt;/a&gt; that I found helpful in bridging the gap between how product engineers think and how the more academic aspects of event sourcing work.&lt;/p&gt;

&lt;p&gt;And thanks to the Event Sourcing and CQRS books I read and yet did not understand at all back when I was slinging .NET and Java code early in my career. It didn’t click for me then, but glad to be able to take some parts of it now.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fboringrails.com%2Fimages%2Ftwitter-banner.png"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Writing better Action Mailers: Revisiting a core Rails concept</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Mon, 16 Jan 2023 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/writing-better-action-mailers-revisiting-a-core-rails-concept-2iae</link>
      <guid>https://forem.com/swanson/writing-better-action-mailers-revisiting-a-core-rails-concept-2iae</guid>
      <description>&lt;p&gt;Mailers are a feature used in literally every Rails application. But they are often an after thought where we throw out the rules of well-written applications.&lt;/p&gt;

&lt;p&gt;Writing mailers is a “set it and forget it” part of your codebase. But recently, I’ve revisited the handful of mailers in my application and I was shocked at both how bad things were and also how many nice mailer features in Rails I wasn’t aware of.&lt;/p&gt;

&lt;p&gt;I’ve been writing Rails applications for over 10 years and there were things I figured out &lt;strong&gt;just this week&lt;/strong&gt; about mailers that I will be using as my new defaults going forward.&lt;/p&gt;

&lt;p&gt;Psst! If you like thinking about software and writing code in the "Boring Rails" style, we are &lt;a href="https://www.notion.so/arrows/Product-Engineer-Ruby-on-Rails-b59c7c818d4c447095d54e9171a32bc3" rel="noopener noreferrer"&gt;hiring Product Engineers at Arrows&lt;/a&gt;. Come work with me!&lt;/p&gt;

&lt;h2&gt;
  
  
  Better display names
&lt;/h2&gt;

&lt;p&gt;Rails has a built-in helper for formatting a display name that shows in email clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActionMailer&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"help@arrows.to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Arrows HQ"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Arrows HQ &amp;lt;help@arrows.to&amp;gt;"&lt;/span&gt;

&lt;span class="no"&gt;ActionMailer&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Matt Swanson &amp;lt;matt@boringrails.com&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This might seem trivial but it handles nil values and escaping quotes for you. And it’s a really nice touch for making emails from your app feel more polished.&lt;/p&gt;

&lt;p&gt;I’ve &lt;a href="https://boringrails.com/tips/rails-mailers-email-address-with-name" rel="noopener noreferrer"&gt;written about this helper before&lt;/a&gt; but every time I mention it, someone replies telling them this is the first they’ve heard of it, so I will keep repeating it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing the view folders
&lt;/h2&gt;

&lt;p&gt;One thing that also bugs me is the file structure for mailer views. By default, the view for, e.g. &lt;code&gt;NotificationMailer.welcome_email&lt;/code&gt; will be located at &lt;code&gt;app/views/notification_mailer/welcome_email.html.erb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This structure mirrors how controllers in Rails work. But it makes it difficult for you to see all your email templates at once. Often times, when we make a change to how we display emails, I need to scan all of the templates to make sure we aren’t doing anything funky.&lt;/p&gt;

&lt;p&gt;I came across this post by Andy Croll that shows you how to &lt;a href="https://andycroll.com/ruby/all-your-mailer-views-in-one-place/" rel="noopener noreferrer"&gt;put all your mailer views in one place&lt;/a&gt;. If you make one small tweak to your &lt;code&gt;ApplicationMailer&lt;/code&gt;, you can achieve a more scannable folder structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionMailer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;prepend_view_path&lt;/span&gt; &lt;span class="s2"&gt;"app/views/mailers"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can put your mailer view in &lt;code&gt;app/views/mailers/notification_mailer/welcome_email.html.erb&lt;/code&gt;. It is one extra level of folder nesting, but now you have a specific &lt;code&gt;app/views/mailers&lt;/code&gt; folder instead of mixing in mailer views with controller views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple emails per mailer
&lt;/h2&gt;

&lt;p&gt;Did you know you can put multiple emails inside of a single mailer?&lt;/p&gt;

&lt;p&gt;There is nothing in the Rails documentation that says you can’t have multiple emails from one mailer, but it also isn’t explicitly encouraged. Sometimes you just need explicit permission from a random person on the internet and I am happy to be that person!&lt;/p&gt;

&lt;p&gt;For whatever reason, every Rails app I’ve worked in has a one-to-one relationship between &lt;code&gt;Mailer&lt;/code&gt; classes and email methods. Not only does this make it harder to grok the emails in your system, but it presents weird friction in naming that should jump out as a code smell.&lt;/p&gt;

&lt;p&gt;Previously, I would make mailers like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentReplyMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="s2"&gt;"minimal"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment_reply_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# mail(to: ...)&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;class&lt;/span&gt; &lt;span class="nc"&gt;UserMentionedMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="s2"&gt;"minimal"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mentioned_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mentionee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# mail(to: ...)&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;p&gt;Why? I don’t really know. I can’t defend separating them.&lt;/p&gt;

&lt;p&gt;Instead, you can group functionality into one mailer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="s2"&gt;"minimal"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# mail(to: ...)&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;mentioned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mentionee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# mail(to: ...)&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;p&gt;Now you can see all the notifications in one file. There are so many times when I forget that we have a certain email going out and it doesn’t get updated when a new feature is added to the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  You don’t need to write the word “email”
&lt;/h2&gt;

&lt;p&gt;Mailers send emails. You don’t need to append &lt;code&gt;_email&lt;/code&gt; to your methods/views – especially if you follow the above tip to put the views into &lt;code&gt;app/views/mailers&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# No one is going to be surprised that this code sends an email&lt;/span&gt;
&lt;span class="no"&gt;NotificationMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comment_reply_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;

&lt;span class="c1"&gt;# So don't prefix everything with `email`!&lt;/span&gt;
&lt;span class="no"&gt;NotificationMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comment_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parameterized mailers
&lt;/h2&gt;

&lt;p&gt;Okay, so far all of these tips are neat but nothing too wild.&lt;/p&gt;

&lt;p&gt;Recently, I’ve been building a feature that allows you to send transactional from your own sending domain. So instead of getting a system email from &lt;a href="//mailto:hello@arrows.to"&gt;hello@arrows.to&lt;/a&gt;, it would come from &lt;a href="//mailto:onboarding@acme-saas.com"&gt;onboarding@acme-saas.com&lt;/a&gt;. There is some DNS related stuff that needs to happen in the background but I want to focus on the mailer portion of this feature.&lt;/p&gt;

&lt;p&gt;One difficulty was making sure that, if an account had setup the custom sending domain, we overwrite the &lt;code&gt;From&lt;/code&gt; address in the mailer. The problem was that there are many mailers in the application and I didn’t want to put this conditional code in every mailer. I was worried that – down the line – I would add a new email and then forget to consider the case where the account is using a custom sending domain.&lt;/p&gt;

&lt;p&gt;I started with this basic implementation in one of the impacted mailers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="n"&gt;build_from_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;account&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="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_from_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class="n"&gt;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_name&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello@arrows.to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&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="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;p&gt;My first thought was that I could add some code to my &lt;code&gt;ApplicationMailer&lt;/code&gt; to check the &lt;code&gt;Current.account&lt;/code&gt;. But I hit a snag because mailers should be sent in a background job (via &lt;code&gt;deliver_later&lt;/code&gt;) and we lose the context of the current request (and thus, the &lt;code&gt;Current&lt;/code&gt; attributes). There are ways around this – like a middleware to pass along the &lt;code&gt;Current&lt;/code&gt; attributes to the job – but something felt off to me.&lt;/p&gt;

&lt;p&gt;When looking for a better way to implement this, I went back to the &lt;a href="https://guides.rubyonrails.org/action_mailer_basics.html" rel="noopener noreferrer"&gt;Action Mailer documentation&lt;/a&gt; and I noticed something: none of the example code was passing in data to the mailer methods directly. Instead there were accessing everything via &lt;code&gt;params&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;NotificationMailer&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comment_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You would write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;NotificationMailer&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&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;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;comment: &lt;/span&gt;&lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comment_reply&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had never used this pattern before. Mailers have not changed since I first learned Rails (in the 2.x days!) but in Rails 5.1, the concept of a &lt;a href="https://github.com/rails/rails/blob/8a7a9d0a0b27e113e6d0a6087299c4bd41d29738/guides/source/5_1_release_notes.md#parameterized-mailers" rel="noopener noreferrer"&gt;“parameterized” mailer was introduced&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My first impression was that I didn’t quite understand the point of this. I generally prefer having the explicit method arguments on the mailer method compared to a generic &lt;code&gt;params&lt;/code&gt; hash.&lt;/p&gt;

&lt;p&gt;But, this time it finally clicked!&lt;/p&gt;

&lt;p&gt;The extra benefit of using &lt;code&gt;with&lt;/code&gt; and parameterized mailers is that you can add &lt;code&gt;before_action&lt;/code&gt; callbacks to your mailers to configure options like the custom sending domain outside of the context of the mailer method.&lt;/p&gt;

&lt;p&gt;I used this concept to make a small change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt; &lt;span class="o"&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;:account&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_from_address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;subject: &lt;/span&gt;&lt;span class="s2"&gt;"New reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="vi"&gt;@from&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;mentioned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mentionee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="n"&gt;mentionee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;subject: &lt;/span&gt;&lt;span class="s2"&gt;"You were mentioned"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="vi"&gt;@from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&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;build_from_address&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class="n"&gt;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_name&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello@arrows.to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@account&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="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;p&gt;So far, this isn’t really much better but this is setting the stage for being able to pull this configuration out of each mailer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic defaults
&lt;/h2&gt;

&lt;p&gt;The next piece of the puzzle is making use of mailer &lt;code&gt;default&lt;/code&gt; options. One thing you might not realize (because I didn’t either…) is that you can pass a lambda to &lt;code&gt;default&lt;/code&gt; to set the value dynamically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="c1"&gt;# You can pass in a static value&lt;/span&gt;
  &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="s2"&gt;"hello@arrows.to"&lt;/span&gt;

  &lt;span class="c1"&gt;# But...it's probably more useful to use dynamic values&lt;/span&gt;
  &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;build_default_from_address&lt;/span&gt; &lt;span class="p"&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;build_default_from_address&lt;/span&gt;
    &lt;span class="c1"&gt;# Construct the default from address here&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;p&gt;The nice part about using &lt;code&gt;default&lt;/code&gt; is that individual mailers can override the &lt;code&gt;from&lt;/code&gt; option if they need to, but if you set the default, you can omit it completely.&lt;/p&gt;

&lt;p&gt;The key breakthrough for my implementation of the custom sending domain feature comes from combining the dynamic defaults with the parameterized mailer option for passing in data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting the dots
&lt;/h2&gt;

&lt;p&gt;By using dynamic defaults and &lt;code&gt;before_action&lt;/code&gt; callbacks, you can have access to the &lt;code&gt;params&lt;/code&gt; hash when configuring the defaults.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;build_default_from_address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt; &lt;span class="o"&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;:account&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;comment_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;subject: &lt;/span&gt;&lt;span class="s2"&gt;"New reply"&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;mentioned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mentionee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="n"&gt;mentionee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;subject: &lt;/span&gt;&lt;span class="s2"&gt;"You were mentioned"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&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;build_from_address&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class="n"&gt;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_name&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello@arrows.to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@account&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="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="no"&gt;NotificationMailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;account: &lt;/span&gt;&lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;comment_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;deliver_later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the logic for the custom sending address has been completely removed from the mailer methods. And it now clear that this behavior is not specific to just the &lt;code&gt;NotificationMailer&lt;/code&gt;. Since we pulled the code into callbacks and parameterized options, we can hoist it up to a new mailer base class.&lt;/p&gt;

&lt;p&gt;This also allows me to introduce the concept of an “Account scoped email” – an email sent in the context of a specific Account, which may have additional configuration or features.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="s2"&gt;"minimal"&lt;/span&gt;
  &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;build_default_from_address&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt; &lt;span class="o"&gt;=&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;:account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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;build_from_address&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class="n"&gt;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_email_name&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hello@arrows.to"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@account&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="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;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;AccountMailer&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;DigestMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;AccountMailer&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;ParticipationMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;AccountMailer&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;p&gt;There is one subtle change that also improves the developer experience and ensures that future mailers don’t omit the &lt;code&gt;with(account: @account)&lt;/code&gt; configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt; &lt;span class="o"&gt;=&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;:account&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;p&gt;The &lt;code&gt;before_action&lt;/code&gt; will fail with a &lt;code&gt;NoMethodError&lt;/code&gt; if the &lt;code&gt;params&lt;/code&gt; are omitted and by using &lt;code&gt;fetch&lt;/code&gt; it will fail with &lt;code&gt;KeyError: :account&lt;/code&gt; if the caller does not pass in the &lt;code&gt;account&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap it up
&lt;/h2&gt;

&lt;p&gt;Mailers are the worst part of a Rails application in terms of quality. While the framework provides an extremely powerful and elegant conceptual compression around sending emails, we often write them once and never touch them in our application code.&lt;/p&gt;

&lt;p&gt;We frequently ignore complex code and duplication in mailers because they are at the edge of the system. The difficulty in rendering HTML email views does not help and further encourages “get it working and never touch it again” behavior.&lt;/p&gt;

&lt;p&gt;But you the tools for organizing mailers just like the rest of your codebase.&lt;/p&gt;

&lt;p&gt;I hadn’t brushed up mailers since I first learned Rails and I was surprised by how much I could improve them with a couple of small changes.&lt;/p&gt;

&lt;p&gt;In my specific case, I was able to abstract a cross-cutting behavior (customizing the sender address) into a base mailer class. By using dynamic defaults and parameterized mailers, I can provide a pleasant developer experience that makes it easy to do “the right thing” for future code.&lt;/p&gt;

&lt;p&gt;By applying some extra thought on naming and folder structure, I was able to make the mailer methods read better and make it easier to see the full scope of email views in the app. And by grouping multiple emails into single mailer classes, you can keep things that are similar close together in the code.&lt;/p&gt;

&lt;p&gt;As I move forward, I will be working to define more application specific mailer contexts: for example an &lt;code&gt;AccountMailer&lt;/code&gt; base class for emails generated within the scope of an account and &lt;code&gt;SystemMailer&lt;/code&gt; base class for things like login and password reset emails that have different configuration options.&lt;/p&gt;

&lt;p&gt;Your mailers can be an exemplary part of your codebase with a little bit of work!&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqc0dnbt79a9hq4zd55v.png" width="722" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Thinking in Hotwire: Progressive Enhancement</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Tue, 16 Aug 2022 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/thinking-in-hotwire-progressive-enhancement-34b8</link>
      <guid>https://forem.com/swanson/thinking-in-hotwire-progressive-enhancement-34b8</guid>
      <description>&lt;p&gt;This post is part of &lt;a href="https://dev.to/hotwire-summer"&gt;Hotwire Summer&lt;/a&gt;: a new season of content on Boring Rails!&lt;/p&gt;

&lt;p&gt;There are many tutorials about how to get started with &lt;a href="https://hotwired.dev/"&gt;Hotwire&lt;/a&gt; and how to use the individual pieces. But one thing that took me a while to grasp was how to “think in Hotwire”.&lt;/p&gt;

&lt;p&gt;Hotwire itself is an overarching concept (HTML-over-the-wire) and you’ll need to know when to use the different pieces (&lt;a href="https://turbo.hotwired.dev/reference/drive"&gt;Turbo Drive&lt;/a&gt;, &lt;a href="https://turbo.hotwired.dev/reference/frames"&gt;Frames&lt;/a&gt;, &lt;a href="https://turbo.hotwired.dev/reference/streams"&gt;Streams&lt;/a&gt;, &lt;a href="https://stimulus.hotwired.dev/"&gt;Stimulus&lt;/a&gt;, &lt;a href="https://turbo.hotwired.dev/handbook/native"&gt;Turbo Native&lt;/a&gt;, &lt;a href="https://github.com/hotwired/turbo-ios/blob/main/Docs/Advanced.md#native---javascript-integration"&gt;Strada&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Because Hotwire is a collection of tools, you can solve problems multiple ways. There are features you can build with Frames that you could also build with Streams. You can always drop to a “lower level” tool and make something work.&lt;/p&gt;

&lt;p&gt;So how should you know when to reach for each tool?&lt;/p&gt;

&lt;p&gt;I think the best approach comes from the earliest days of web development: &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement"&gt;progressive enhancement&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaffolds and CRUD without Hotwire
&lt;/h2&gt;

&lt;p&gt;Let’s start with the absolute bare bones: the pre-Hotwire days of Rails. You would scaffold out a resource-based controller.&lt;/p&gt;

&lt;p&gt;To add a comment to a post, you would have a &lt;code&gt;comments_controller&lt;/code&gt; with a &lt;code&gt;new&lt;/code&gt; action to render a form.&lt;/p&gt;

&lt;p&gt;You would submit a new comment to &lt;code&gt;comments#create&lt;/code&gt; where the comment model would be saved and then we send a redirect back to the &lt;code&gt;post#show&lt;/code&gt; route.&lt;/p&gt;

&lt;p&gt;It’s as boring as you can get, but it works and is the foundation upon which RESTful web applications were built.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FYT0Bu7o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/scaffold-crud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FYT0Bu7o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/scaffold-crud.png" alt="" width="880" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Turbo Drive
&lt;/h2&gt;

&lt;p&gt;So what if we wanted to enhance the experience just the tiniest amount? We could the first layer of Hotwire: Turbo Drive. If you’re on a recent version of Rails, Turbo Drive is on by default, even if you didn’t realize it.&lt;/p&gt;

&lt;p&gt;Now when you click &lt;code&gt;+ Add&lt;/code&gt;, instead of a full-page reload of &lt;code&gt;comments#new&lt;/code&gt;, we use Turbo Drive to fetch the page via AJAX and then Turbo Drive swaps out the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; contents. This behavior (originally called Turbolinks) mimics the responsiveness of a single-page application, but everything is still server-rendered in Rails.&lt;/p&gt;

&lt;p&gt;Turbo Drive does the same thing when you submit the comment form: instead of a full page reload, the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; is swapped, even when we do a &lt;code&gt;redirect&lt;/code&gt;. The JS and Rails portions of Turbo Drive work together to make this seamless (…assuming you follow the right conventions!).&lt;/p&gt;

&lt;p&gt;So you could stop right here and technically you are using Hotwire. Turbo Drive is mostly invisible for Rails developers. If you don’t need any more interactivity, you don’t need to go any deeper.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DKJ-Lgze--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-drive.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DKJ-Lgze--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-drive.png" alt="" width="880" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Turbo Frames
&lt;/h2&gt;

&lt;p&gt;While Turbo Drive originated as Turbolinks, Turbo Frames are a new concept in Hotwire. I’ve described Turbo Frames before as “iFrames but they work how you would want them to”. In Hotwire, Turbo Frames are used for partial page updates.&lt;/p&gt;

&lt;p&gt;A Frame works similar to Turbo Drive: navigation visits and form submissions are intercepted, made via AJAX, and then the response is swapped into the page. But with Frames, instead of swapping the whole &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag, only the contents of a matching Turbo Frame is swapped.&lt;/p&gt;

&lt;p&gt;So if you have a link instead a Turbo Frame with an &lt;code&gt;id&lt;/code&gt; of &lt;code&gt;comment_123&lt;/code&gt;, Hotwire will look for a matching &lt;code&gt;&amp;lt;turbo-frame id='comment_123'&amp;gt;&lt;/code&gt; tag in the response to swap.&lt;/p&gt;

&lt;p&gt;The canonical use-case is inline editing. If you have a list of comments and you wrap a frame around each comment, you can add a edit button. Clicking the edit button can render a &lt;code&gt;comments#edit&lt;/code&gt; response, but swap out the form with the frame instead of loading a separate page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A_uFlUUh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-frame.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A_uFlUUh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-frame.png" alt="" width="880" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Turbo Streams
&lt;/h2&gt;

&lt;p&gt;Turbo Drive replaces the whole &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag. Turbo Frames replace a frame. Turbo Streams go even more granular.&lt;/p&gt;

&lt;p&gt;Turbo Streams provide a set of standard operations for manipulating the HTML on the page. You can add, remove, or replace content.&lt;/p&gt;

&lt;p&gt;In our comment example, if you want to delete a comment, you can respond to the request with a Turbo Stream to remove the comment from the page. Or if you create a comment, you can add it to the end of the list.&lt;/p&gt;

&lt;p&gt;Turbo Streams are the Hotwire version of another classic Rails technique: server-generated JavaScript response (SJR). In the past, you could return back a snippet of JavaScript that would get executed in the page. But this caused issues with Content Security Policies and was a bit too permissive.&lt;/p&gt;

&lt;p&gt;Turbo Streams provides a CRUD-like abstraction to handle nearly anything you &lt;strong&gt;should&lt;/strong&gt; have been doing with SJR.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vdnk0Dx8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-stream.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vdnk0Dx8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-stream.png" alt="" width="880" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Turbo Streams + Action Cable
&lt;/h2&gt;

&lt;p&gt;Turbo Stream responses can also be broadcast out-of-band using &lt;a href="https://guides.rubyonrails.org/action_cable_overview.html"&gt;Action Cable&lt;/a&gt;. One major point of confusion is that Turbo Streams do not &lt;em&gt;have&lt;/em&gt; to be sent over Action Cable or web sockets, you can use them within a normal HTTP request/response cycle.&lt;/p&gt;

&lt;p&gt;But let’s say we want real-time functionality so that when &lt;strong&gt;someone else&lt;/strong&gt; adds a comment, it gets added to our view of the page without doing a refresh.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XXctnwP8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-stream-cable.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XXctnwP8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-stream-cable.png" alt="" width="880" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Stimulus
&lt;/h2&gt;

&lt;p&gt;Hotwire guides you to write as much of your application using server rendered Ruby code as possible. But there are cases where you still want extra sprinkles of JavaScript functionality on the client side.&lt;/p&gt;

&lt;p&gt;Stimulus is the recommended JavaScript framework for attaching functions to your HTML markup and responding to simple browser events.&lt;/p&gt;

&lt;p&gt;One thing to note is that Stimulus does not provide anything to support client side rendering. There are no templates or JSX. If you find yourself generating a lot of HTML in your Stimulus controllers, you should take a step back and re-assess.&lt;/p&gt;

&lt;p&gt;Stimulus controllers are often general-purpose and under 50 lines of code.&lt;/p&gt;

&lt;p&gt;If you want to support collapsing comments, you could add a simple Stimulus controller to show or expand a comment. (Note: in this specific case, a Hotwire enthusiast might reach for native HTML elements like &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"&gt;&lt;code&gt;&amp;lt;detail&amp;gt;&lt;/code&gt;&lt;/a&gt; but I digress…)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NMABf5Tb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/stimulus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NMABf5Tb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/stimulus.png" alt="" width="880" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding custom components / React / client side JavaScript
&lt;/h2&gt;

&lt;p&gt;There is a time and place for tools like React for high interactivity components. Whether you use a tool like React, Vue or Web Components, there are things that are best built on the client side that go beyond what Stimulus can handle.&lt;/p&gt;

&lt;p&gt;For this, you can add a small, isolated component to the page. An example from Rails is the &lt;a href="https://trix-editor.org/"&gt;Trix rich text editor&lt;/a&gt;: it is a standard &lt;code&gt;&amp;lt;trix-editor&amp;gt;&lt;/code&gt; web component.&lt;/p&gt;

&lt;p&gt;In our comment example, maybe you want to add an emoji picker or some kind of fancy drag and drop file uploader. You might wrap these richer JavaScript libraries in a Stimulus controller to help manage Turbo lifecycle events.&lt;/p&gt;

&lt;p&gt;Hotwire advocates don’t say that you should &lt;em&gt;never&lt;/em&gt; write JavaScript components, but rather that you should start with the other tools and only reach for this level when you really need it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4PB_1csp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/custom-elements.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4PB_1csp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/custom-elements.png" alt="" width="880" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Turbo Native
&lt;/h2&gt;

&lt;p&gt;Once you’ve built a feature in your web application, you can use the Turbo Native iOS and Android adapters to create mobile applications that wrap display your same app content inside of a web view.&lt;/p&gt;

&lt;p&gt;This is not a “mobile optimized web app” but a real Swift or Kotlin app that renders pages from your Rails app.&lt;/p&gt;

&lt;p&gt;There are some conveniences to create native action bars and buttons.&lt;/p&gt;

&lt;p&gt;The main idea is to re-use as much of your Rails views as possible and then Turbo Native provides a mechanism to eject and write some screens in the platform native languages. Again, it’s the idea of progressive enhancement.&lt;/p&gt;

&lt;p&gt;The flagship example app (the Hey email client) for instance has a fully native inbox screen, but many of the secondary or account setting screens are wrappers around Rails views.&lt;/p&gt;

&lt;p&gt;The Turbo Native functionality should still be considered as a beta release. Most developers writing Hotwire applications are not using the native features, but it is nice to know that some folks are laying the groundwork for this style of development.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A-P8fCO1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-native.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A-P8fCO1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/progressive/turbo-native.png" alt="" width="880" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Strada
&lt;/h2&gt;

&lt;p&gt;The last piece of Hotwire is the [as of summer 2022] unreleased Strada library. This is an extension of the Turbo Native functionality and helps bridge the gap when you need to communicate between the Swift or Kotlin parts of your application and the HTML/Rails portions.&lt;/p&gt;

&lt;p&gt;This library will be a convenience extension to Turbo Native, it doesn’t add anything fundamentally new to the mix.&lt;/p&gt;

&lt;p&gt;Until the project is actually released, it’s fine to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap it up
&lt;/h2&gt;

&lt;p&gt;Hotwire builds on the idea of progressive enhancement. You should use the least amount of the tooling as possible to achieve your desired outcome. As you move “down the stack” of what Hotwire offers, you trade off more power for more complexity.&lt;/p&gt;

&lt;p&gt;The nice part about this approach is that you can build versions of features quickly, test and iterate based on feedback, and then layer on more real-time and interactive functionality as needed.&lt;/p&gt;

&lt;p&gt;The progressive enhancement approach pairs beautifully with HTML and Hotwire encourages you to explore what can be done with native browser elements and only layers on extra tooling to complement or upgrade the platform.&lt;/p&gt;

&lt;p&gt;Hotwire as a brand is an umbrella for several different tools and hopefully this overview will help you know how far down to reach and how the individual tools come together to build a cohesive and compelling stack.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0llgMsYy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/twitter-banner.png" alt="" width="722" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>hotwire</category>
    </item>
    <item>
      <title>Galaxy brain CSS tricks with Hotwire and Rails</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Tue, 26 Jul 2022 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/galaxy-brain-css-tricks-with-hotwire-and-rails-2eoa</link>
      <guid>https://forem.com/swanson/galaxy-brain-css-tricks-with-hotwire-and-rails-2eoa</guid>
      <description>&lt;p&gt;This post is part of &lt;a href="https://dev.to/hotwire-summer"&gt;Hotwire Summer&lt;/a&gt;: a new season of content on Boring Rails!&lt;/p&gt;

&lt;p&gt;In Hotwire applications, you need to lean more on the fundamentals of CSS and HTML. If you’re like me, you probably learned just enough CSS to get by, but never reach for it first. But that’s changed recently and I wanted to share patterns I’ve picked up recently that improve my Rails apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empty States and Turbo Streams
&lt;/h2&gt;

&lt;p&gt;An extremely common pattern in Rails apps is rendering a collection of elements and if the collection is empty, render an empty state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&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;"my_list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col divide-y"&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="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"list_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;collection: &lt;/span&gt;&lt;span class="vi"&gt;@list&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="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
      Whoops! you have no items!
    &lt;span class="nt"&gt;&amp;lt;/p&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;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine when rendering a typical page, but if you use Turbo Streams to add or remove list items, you’ll find a problem.&lt;/p&gt;

&lt;p&gt;If you had two items in the list, and you remove both via Turbo Streams, the container will be empty but you won’t have rendered the empty state. And if the list is empty and you dynamically append an item, you’ll want to remove the empty state.&lt;/p&gt;

&lt;p&gt;You could re-render the whole list instead of inserting items, but one technique that I’ve found helpful is using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child" rel="noopener noreferrer"&gt;CSS &lt;code&gt;only-child&lt;/code&gt; pseudo-selector&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ll show examples with Tailwind (because Tailwind is &lt;a href="https://twitter.com/_swanson/status/1550557798513131521" rel="noopener noreferrer"&gt;really, really good&lt;/a&gt;), but the same concept applies in regular CSS.&lt;/p&gt;

&lt;p&gt;The idea is to always render the empty state and then use CSS to only show it if there are no items.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&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;"my_list"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col divide-y"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"only:block hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Whoops! you have no items!&lt;span class="nt"&gt;&amp;lt;/p&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="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"list_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;collection: &lt;/span&gt;&lt;span class="vi"&gt;@list&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using Tailwind’s &lt;code&gt;only&lt;/code&gt; modifier we set the empty state to have display &lt;code&gt;block&lt;/code&gt; if it is the only child of the container, otherwise hide it.&lt;/p&gt;

&lt;p&gt;Now you can stream back operations to append or remove items to the &lt;code&gt;my_list&lt;/code&gt; container and let CSS handle hiding or showing the loading state.&lt;/p&gt;

&lt;p&gt;Note: you may want to use &lt;code&gt;last-child&lt;/code&gt;, &lt;code&gt;first-of-type&lt;/code&gt;, or some other modifier depending on your specific markup. Give it a shot!&lt;/p&gt;

&lt;h2&gt;
  
  
  Tailwind Variants with Data Attributes
&lt;/h2&gt;

&lt;p&gt;Hotwire, especially Stimulus, makes use of HTML data attributes heavily. One neat trick is using data attributes to reduce conditionals in views.&lt;/p&gt;

&lt;p&gt;Let’s say you have a list of comments and only admins can delete the comments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&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;div&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="vi"&gt;@comment&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;p&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&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="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&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="s2"&gt;"Delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&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;p&gt;You could instead use a data attribute on the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; of your page to conditionally show admin-related things.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="s1"&gt;'data-admin'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then write styles based on that attribute. Tailwind makes it easy to add custom variants for this via the &lt;code&gt;plugins&lt;/code&gt; section of the Tailwind config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;addVariant&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;addVariant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body[data-admin] &amp;amp;&lt;/span&gt;&lt;span class="dl"&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;p&gt;With this config change you can now use &lt;code&gt;admin:&lt;/code&gt; with any Tailwind classes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&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;div&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="vi"&gt;@comment&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;p&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"admin:block hidden"&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;button_to&lt;/span&gt; &lt;span class="s2"&gt;"Delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :delete&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&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;p&gt;But wait? Isn’t this super risky because someone could just fiddle with the HTML and delete a comment? Well, yes, they could – but you need to be checking authorization on the server-side anyways. It’s a trade-off but there are cases where this cleans up a ton of conditional view logic.&lt;/p&gt;

&lt;p&gt;Shout-out to my friend &lt;a href="https://twitter.com/marckohlbrugge" rel="noopener noreferrer"&gt;Marc Kohlbrugge&lt;/a&gt; for sharing the idea on &lt;a href="https://twitter.com/marckohlbrugge/status/1544113650134384640" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;In our app we recently used this technique to change how much margin we needed on an element based on user roles. Certain roles have a fixed height header that we needed to account for. Instead of a bunch of conditionals, all we had to end up writing was &lt;code&gt;viewer:top-0 editor:top-12&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dynamic styles with erb
&lt;/h2&gt;

&lt;p&gt;Don’t forget that you can use generate &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags dynamically!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-user&lt;/span&gt;&lt;span class="o"&gt;~=&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;yellow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&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;span&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;user: &lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&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;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could use a little CSS to highlight comments that you made with a yellow background by targeting a &lt;code&gt;data-user&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;This is actually an &lt;a href="https://signalvnoise.com/posts/3112-how-basecamp-next-got-to-be-so-damn-fast-without-using-much-client-side-ui" rel="noopener noreferrer"&gt;old Rails technique from Basecamp&lt;/a&gt; that was popular because it works really well with fragment caching. You can cache the same chunk of HTML and then use CSS to change the styles instead of needing multiple, slightly different cache entries.&lt;/p&gt;

&lt;p&gt;I’ve also used this concept for building custom theming features by taking advantage of CSS variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;brand_color&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--color-brand-contrast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="no"&gt;ColorHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;brand_color&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--color-brand-tint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="no"&gt;ColorHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;brand_color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use this to define custom Tailwind colors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgb(var(--color-brand) / &amp;lt;alpha-value&amp;gt;)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brand-contrast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgb(var(--color-brand-contrast) / &amp;lt;alpha-value&amp;gt;)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brand-tint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;var(--color-brand-tint)&lt;/span&gt;&lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now you can use &lt;code&gt;bg-brand&lt;/code&gt; or &lt;code&gt;text-brand-contrast&lt;/code&gt; in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop string interpolating class names!
&lt;/h2&gt;

&lt;p&gt;You will be writing a lot more HTML markup in Hotwire apps: more views, more partials, more components. Make sure you are taking advantage of newer Rails features for generating HTML without doing a bunch of gross string interpolation.&lt;/p&gt;

&lt;p&gt;If you’re writing markup like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-50 p-2 text-gray-700 &lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="s1"&gt;'line-through'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;completed?&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="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please stop! It’s hard to read and tricky to match all of the closing punctuation. There are better ways!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&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;li&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bg-gray-50 p-2 text-gray-700"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"line-through"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="vi"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;completed?&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="vi"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&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;p&gt;&lt;a href="https://twitter.com/_swanson/status/1341080006169137152" rel="noopener noreferrer"&gt;Rails 6.1&lt;/a&gt; added a &lt;code&gt;class_names&lt;/code&gt; helper method and the &lt;code&gt;tag&lt;/code&gt; builder will automatically use it for conditionally setting class values. It’s awesome!&lt;/p&gt;

&lt;p&gt;It’s extra powerful when using a library like ViewComponent where you have a lot of conditional styles, or just want to group up utility classes in a more organized manner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyWidget&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="o"&gt;...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;container_classes&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"flex items-center justify-center space-x-2 rounded-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"disabled:pointer-events-none disabled:select-none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"font-medium tracking-wide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text-white bg-black hover:bg-neutral-900"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:primary&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text-neutral-600 border hover:bg-neutral-50 hover:text-neutral-900"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:secondary&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"text-neutral-600 hover:text-neutral-900 hover:bg-neutral-50"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:tertiary&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"w-full"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;full_width?&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;h2&gt;
  
  
  Wrap it up
&lt;/h2&gt;

&lt;p&gt;CSS is often one of the last tools Rails developers reach for when trying to solve a tricky problem. We are much more inclined to add conditionals to views or fall back to string interpolation to “make it work”. But there are a few techniques that can make working with CSS in your Rails app improve the readability and durability of your code.&lt;/p&gt;

&lt;p&gt;Even though Hotwire’s servered rendered approach feels retro, remember that we don’t have to use CSS like it’s 1998 anymore!&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fboringrails.com%2Fimages%2Ftwitter-banner.png"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>hotwire</category>
      <category>css</category>
    </item>
    <item>
      <title>Adding keyboard shortcuts and hotkeys to StimulusJS</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Mon, 11 Jul 2022 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/adding-keyboard-shortcuts-and-hotkeys-to-stimulusjs-1off</link>
      <guid>https://forem.com/swanson/adding-keyboard-shortcuts-and-hotkeys-to-stimulusjs-1off</guid>
      <description>&lt;p&gt;This post is part of &lt;a href="https://dev.to/hotwire-summer"&gt;Hotwire Summer&lt;/a&gt;: a new season of content on Boring Rails!&lt;/p&gt;

&lt;p&gt;Keyboard shortcuts are a great way to level up your user experience and improve the accessibility of your web applications. Even if you aren’t ready for advanced hotkey schemes, small things like binding the &lt;code&gt;Escape&lt;/code&gt; key to close a modal can have a big impact.&lt;/p&gt;

&lt;p&gt;Stimulus doesn’t come with built-in support for hotkeys. As part of a recent project at &lt;a href="https://arrows.to/"&gt;Arrows&lt;/a&gt;, I evaluated the ecosystem and wanted to share my thoughts.&lt;/p&gt;

&lt;h2&gt;
  
  
  stimulus-hotkeys
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/leastbad/stimulus-hotkeys"&gt;This package&lt;/a&gt; provides a &lt;code&gt;hotkeys&lt;/code&gt; controller and uses a JSON object to map keyboard shortcuts into Stimulus actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"hotkeys"&lt;/span&gt;
  &lt;span class="na"&gt;data-hotkeys-bindings-value=&lt;/span&gt;&lt;span class="s"&gt;'{"ctrl+z, command+z": "#foo-&amp;gt;editor#undo"}'&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&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;"foo"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"editor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syntax of the &lt;code&gt;bindings&lt;/code&gt; map is similar to the Stimulus action syntax, with the addition of a preceding &lt;code&gt;selector&lt;/code&gt; to find the Stimulus controller to invoke the action on. Internally, &lt;code&gt;stimulus-hotkeys&lt;/code&gt; uses the more general &lt;a href="https://wangchujiang.com/hotkeys/"&gt;HotKeys.JS library&lt;/a&gt; so it supports all kinds of fancy combinations, modifiers, and scopes.&lt;/p&gt;

&lt;p&gt;I liked that the shortcut mapping exists in the HTML markup; this felt very much in the spirit of Hotwire. This library adds no extra code to your Stimulus controllers, which is ideal if you are combining together other third-party controllers.&lt;/p&gt;

&lt;p&gt;Unfortunately, adding a hash-like data structure as an HTML attribute is tricky and I don’t love the JSON syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  stimulus-use/useHotkeys
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;stimulus-use&lt;/code&gt; &lt;a href="https://github.com/stimulus-use/stimulus-use/"&gt;project&lt;/a&gt; is a collection of reusable behaviors for Stimulus. If you are familiar with React, this project is similar to React’s &lt;code&gt;hooks&lt;/code&gt; system, but for Stimulus controllers.&lt;/p&gt;

&lt;p&gt;Included in this collection is &lt;a href="https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-hotkeys.md"&gt;useHotkeys&lt;/a&gt;. As with &lt;code&gt;stimulus-hotkeys&lt;/code&gt;, the heavy lifting is done by &lt;a href="https://wangchujiang.com/hotkeys/"&gt;HotKeys.JS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here you need to define the hotkeys and respective handlers inside of your own Stimulus controllers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&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;@hotwired/stimulus&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;useHotkeys&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;stimulus-use&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;useHotkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cmd+t&lt;/span&gt;&lt;span class="dl"&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openPalette&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editFile&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;p&gt;This library felt right at home in a Stimulus controller and it handles unregistering the shortcuts when a controller is disconnected automatically. It was super clear what hotkeys were configured and this felt like a great option for adding hotkeys to your own application controllers.&lt;/p&gt;

&lt;p&gt;One downside that was specific to our needs was that I had a controller that was always be on the page, but should not respond to hotkeys when hidden. While &lt;code&gt;useHotkey&lt;/code&gt;s handles the Stimulus controller lifecycle, it does make it inflexible when it comes to binding and unbinding the keyboard events.&lt;/p&gt;

&lt;h2&gt;
  
  
  HotKeys.JS (directly)
&lt;/h2&gt;

&lt;p&gt;After exploring the two most popular Stimulus-specific solutions and realizing they both used the same underlying dependency, I decided to go straight to the &lt;a href="https://wangchujiang.com/hotkeys/"&gt;source&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&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;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;hotkeys&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;hotkeys-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hotkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esc&lt;/span&gt;&lt;span class="dl"&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;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hotkeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unbind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esc&lt;/span&gt;&lt;span class="dl"&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;p&gt;Adding the library directly is straightforward. You do need to be careful to clean up your event listeners by calling &lt;code&gt;unbind&lt;/code&gt; or else you’ll end up with multiple instances of the hotkey functions getting created.&lt;/p&gt;

&lt;p&gt;For very simple cases (one or two basic hotkeys), this approach is perfectly valid. In our application, I ended up going this route as I needed more fine-grained control of when the shortcuts were bound/unbound.&lt;/p&gt;

&lt;h2&gt;
  
  
  github/hotkey
&lt;/h2&gt;

&lt;p&gt;The last option I evaluated was a small library from GitHub called &lt;a href="https://github.com/github/hotkey"&gt;github/hotkey&lt;/a&gt;. It is not Stimulus / Hotwire specific.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-hotkey=&lt;/span&gt;&lt;span class="s"&gt;"Shift+?"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Show help dialog&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/page/2"&lt;/span&gt; &lt;span class="na"&gt;data-hotkey=&lt;/span&gt;&lt;span class="s"&gt;"j"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Next&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/help"&lt;/span&gt; &lt;span class="na"&gt;data-hotkey=&lt;/span&gt;&lt;span class="s"&gt;"Control+h"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Help&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/rails/rails"&lt;/span&gt; &lt;span class="na"&gt;data-hotkey=&lt;/span&gt;&lt;span class="s"&gt;"g c"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Code&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/search"&lt;/span&gt; &lt;span class="na"&gt;data-hotkey=&lt;/span&gt;&lt;span class="s"&gt;"s,/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found this library via the sourcemaps of &lt;a href="https://www.hey.com/"&gt;HEY.com&lt;/a&gt; and it has been battle-tested by GitHub in the main Rails app (from the README: “This is used on almost every page on GitHub.”).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;data-hotkey&lt;/code&gt; attribute has the cleanest syntax of any of the options. You would want a wrapper Stimulus controller to handle lifecycle events, but it would be only for calling &lt;code&gt;install&lt;/code&gt; and &lt;code&gt;uninstall&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The design of this library relies more heavily on browser default behaviors. Instead of calling arbitrary JavaScript functions (or Stimulus actions), &lt;code&gt;github/hotkey&lt;/code&gt; triggers the action on the target HTML element: links would trigger a navigation visit, buttons would get submitted, input fields would get focus, etc.&lt;/p&gt;

&lt;p&gt;If you can structure your markup to use native browser functionality, this is a great option. But there isn’t a way to hook into your existing Stimulus actions directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap it up
&lt;/h2&gt;

&lt;p&gt;It really depends on what your specific hotkey needs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;stimulus-hotkeys&lt;/code&gt; is a drop-in tool that doesn’t require any extra JavaScript&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stimulus-use/useHotKeys&lt;/code&gt; is a convenient Stimulus-native API wrapper around HotKey.js&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HotKey.js&lt;/code&gt; is a powerful abstraction around key events that you can mix into your own code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;github/hotkey&lt;/code&gt; leans heavily on native browser behavior and has the nicest syntax&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, you’ll need to navigate the trade-offs for what you’re trying to build. In my case, I went with directly using &lt;code&gt;HotKey.js&lt;/code&gt; because it best fit my specific requirements.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0llgMsYy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/twitter-banner.png" alt="" width="722" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>The most underrated Rails helper: dom_id</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Tue, 28 Jun 2022 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/the-most-underrated-rails-helper-domid-1ic6</link>
      <guid>https://forem.com/swanson/the-most-underrated-rails-helper-domid-1ic6</guid>
      <description>&lt;p&gt;This post is part of &lt;a href="https://dev.to/hotwire-summer"&gt;Hotwire Summer&lt;/a&gt;: a new season of content on Boring Rails!&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dom_id&lt;/code&gt; helper in Rails is over a decade old, but has proven to be an invaluable concept in Hotwire.&lt;/p&gt;

&lt;p&gt;This secret workhorse powers all kinds of HTML-related behavior in Rails. It has one key job: making it easy to associate application data with DOM elements.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dom_id&lt;/code&gt; takes two arguments: a record and an optional prefix.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;record&lt;/code&gt; can be anything that responds to &lt;code&gt;to_key&lt;/code&gt; and &lt;code&gt;model_name&lt;/code&gt;, but 99% of the time you are passing it an ActiveRecord model. The &lt;code&gt;prefix&lt;/code&gt; can be anything that responds to &lt;code&gt;to_s&lt;/code&gt;, but 99% of the time it is a symbol.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.rubyonrails.org/classes/ActionView/RecordIdentifier.html"&gt;From the docs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "post_45"&lt;/span&gt;
&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&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="c1"&gt;# =&amp;gt; "new_post"&lt;/span&gt;

&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;:edit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "edit_post_45"&lt;/span&gt;
&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Post&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;:custom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "custom_post"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason this helper is so nice is that it &lt;a href="https://rubyonrails.org/doctrine#convention-over-configuration"&gt;sets a convention&lt;/a&gt;. Instead of defining your own way of identifying markup and later hoping you remember the pattern, Rails provides a standard; you don’t need to switch contexts to know if you set an id to “post_23_comments” or “comments-post-23” or wait, was it “post_23-comments”?&lt;/p&gt;

&lt;p&gt;By providing stable id values for your elements, you avoid fragile code like targeting the first element child or finding a node based on the text value.&lt;/p&gt;

&lt;p&gt;Here are places where you should regularly reach for &lt;code&gt;dom_id&lt;/code&gt; when building Hotwire apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Super clean tag builders
&lt;/h2&gt;

&lt;p&gt;Leaning into HTML markup as the source of truth means adding extra attributes and conditionals when rendering templates. While you can do plain old string interpolation in your ERB templates, Rails has a set of &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag"&gt;nice tag builders&lt;/a&gt; that help you avoid drowning in a sea of brackets, braces, and octothorpes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&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;div&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="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comments&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;"flex flex-col divide-y"&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;render&lt;/span&gt; &lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comments&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;p&gt;As you use these helpers more, make sure you check out these tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up the &lt;a href="https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss"&gt;Tailwind Intellisense&lt;/a&gt; plugin for VS Code to &lt;a href="https://twitter.com/_swanson/status/1541419976732692485"&gt;autocomplete when using tag helpers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Take advantage of &lt;a href="https://twitter.com/_swanson/status/1341080006169137152"&gt;class_name helper&lt;/a&gt; (from the &lt;code&gt;classNames&lt;/code&gt; React API) for conditional classes&lt;/li&gt;
&lt;li&gt;For commmon elements, consider extracting a component with a &lt;a href="https://boringrails.com/tips/lightweight-components-with-helpers-stimulus"&gt;lightweight helper&lt;/a&gt; or a library like &lt;a href="https://viewcomponent.org/"&gt;ViewComponent&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deep linking anchor tags
&lt;/h2&gt;

&lt;p&gt;Since Rails leans so heavily on native browser features, make sure you take advantage of anchor tags on links. You can use &lt;code&gt;dom_id&lt;/code&gt; to scroll the browser directly to an element with the corresponding &lt;code&gt;id&lt;/code&gt; (or generate a permalink that users can share).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"View comment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;posts_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;anchor: &lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use this pattern for redirects. For example, after creating an item in a list, you can redirect back to the index page but scroll to the new item.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommentsController&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="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comment_params&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;posts_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;anchor: &lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@comment&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;p&gt;A few more tips related to deep linking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can use the &lt;code&gt;:target&lt;/code&gt; pseudo-class to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:target"&gt;style the element with an ID matching the URL anchor&lt;/a&gt;. In Tailwind, simply use the &lt;a href="https://tailwindcss.com/docs/hover-focus-and-other-states#target"&gt;target prefix&lt;/a&gt; (e.g. &lt;code&gt;target:bg-yellow-50&lt;/code&gt; to add a subtle yellow background to an element when it matches the URL anchor)&lt;/li&gt;
&lt;li&gt;Another handy CSS property is &lt;code&gt;scroll-margin-top&lt;/code&gt;: the browser will scroll the targeted element all the way to the top of the window. You may want a little extra padding, but only when you scrolled to the element. Don’t add extra margin or padding to your designs or add weird wrapper divs…&lt;code&gt;scroll-margin-top&lt;/code&gt; (&lt;a href="https://tailwindcss.com/docs/scroll-margin"&gt;Tailwind class&lt;/a&gt;: &lt;code&gt;scroll-mt&lt;/code&gt;) is the answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  With Turbo Frames
&lt;/h2&gt;

&lt;p&gt;When you start adding Turbo Frames to your application, you’ll need to provide an id for the frame tag. The Rails &lt;code&gt;turbo_frame_tag&lt;/code&gt; uses – you guessed it – &lt;code&gt;dom_id&lt;/code&gt; under the hood. But you can also pass in your own ids as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; &amp;lt;turbo-frame id="post_123"&amp;gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:comments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; &amp;lt;turbo-frame id="comments_post_123"&amp;gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since a Turbo Frame needs to be unique per page, &lt;code&gt;dom_id&lt;/code&gt; is a convenient way to generate frame ids, especially if you have multiple frames on the page.&lt;/p&gt;

&lt;p&gt;And since you can &lt;a href="https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets"&gt;navigate a frame via other links&lt;/a&gt;, it’s a great convention to follow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;comment_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Elsewhere... --&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="s2"&gt;"Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;edit_comment_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@comment&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;turbo_frame: &lt;/span&gt;&lt;span class="vi"&gt;@comment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scoping Turbo Stream responses
&lt;/h2&gt;

&lt;p&gt;Making small mutations on a page with Turbo Streams is super powerful, but since your stream actions are in a separate &lt;code&gt;turbo_stream.erb&lt;/code&gt; file, it can be tricky to match up your ids between views.&lt;/p&gt;

&lt;p&gt;Once again, &lt;code&gt;dom_id&lt;/code&gt; keeps things consistent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/plans/quick_edit/update.turbo_stream.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:title&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;"plans/title"&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;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:notes&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;"plans/notes"&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;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:assigned&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;"plans/assigned"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And by adding the correct ids to your markup, the Turbo Stream responses are super clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/comments/destroy.turbo_stream.erb --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Calls `dom_id(@comment)` under the hood --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt; &lt;span class="vi"&gt;@comment&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap it up
&lt;/h2&gt;

&lt;p&gt;Who would have thought that a simple helper to generate HTML &lt;code&gt;id&lt;/code&gt; values from your application models would be such a useful concept that, more than a decade after first being introduced, it continues to prove helpful even on the newest and shiniest parts of Rails.&lt;/p&gt;

&lt;p&gt;If you haven’t been using &lt;code&gt;dom_id&lt;/code&gt; before, consider it the next time you write a view in your Rails app. You might be surprised at how much more pleasant it is to create HTML when you aren’t littering it with code like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&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;'&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@post&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;-comments"&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;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;a href="https://twitter.com/_swanson"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0llgMsYy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/twitter-banner.png" alt="" width="722" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>hotwire</category>
    </item>
    <item>
      <title>Self-destructing StimulusJS controllers</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Mon, 13 Jun 2022 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/self-destructing-stimulusjs-controllers-3jj0</link>
      <guid>https://forem.com/swanson/self-destructing-stimulusjs-controllers-3jj0</guid>
      <description>&lt;p&gt;This post is part of &lt;a href="https://boringrails.com/hotwire-summer"&gt;Hotwire Summer&lt;/a&gt;: a new season of content on Boring Rails!&lt;/p&gt;

&lt;p&gt;Sometimes you need a little sprinkle of JavaScript to make a tiny UX improvement. In the olden days, full-stack developers would often drop small jQuery snippets straight into the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/javascript"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.flash-container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fadeOut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;highlight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It got the job done, but it wasn’t the best.&lt;/p&gt;

&lt;p&gt;In Hotwire apps you can use a “self-destructing” Stimulus controller to achieve the same result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-destructing?
&lt;/h2&gt;

&lt;p&gt;Self-destructing Stimulus controllers run a bit of code and then remove themselves from the DOM by calling &lt;code&gt;this.element.remove()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s see an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/scroll_to_controller.js&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;Controller&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;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nx"&gt;targetElement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locationValue&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;p&gt;This controller takes in a &lt;code&gt;location&lt;/code&gt; value and then scrolls the page to show that element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt;
  &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"scroll-to"&lt;/span&gt;
  &lt;span class="na"&gt;data-scroll-to-location-value=&lt;/span&gt;&lt;span class="s"&gt;"task_12345"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For self-destructing controllers, I like to use the &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag since &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"&gt;it will not be displayed in the browser&lt;/a&gt; and is a good signal when reading the code that this isn’t just an empty &lt;code&gt;div&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This pattern works really well with &lt;a href="https://turbo.hotwired.dev/handbook/streams"&gt;Turbo Stream&lt;/a&gt; responses.&lt;/p&gt;

&lt;p&gt;Imagine you have a list of task with an inline form to create a new task. You can submit the form and then send back a &lt;code&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; to append to the list and then scroll the page to the newly created task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt; &lt;span class="ss"&gt;:tasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@task&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;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt; &lt;span class="ss"&gt;:tasks&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;template&lt;/span&gt;
    &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"scroll-to"&lt;/span&gt;
    &lt;span class="na"&gt;data-scroll-to-location-value=&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="vi"&gt;@task&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;&amp;lt;/template&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;p&gt;And because we wrap our small bit of JavaScript functionality in a Stimulus controller, all of the lifecycle events are taken care of. No need to listen for &lt;code&gt;turbo:load&lt;/code&gt; events, it just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else could you use this for?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Highlighter
&lt;/h3&gt;

&lt;p&gt;We use this &lt;code&gt;highlighter&lt;/code&gt; controller to add extra styles when something is “selected”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---IL4g6En--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/highlighter-example.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---IL4g6En--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/highlighter-example.png" alt="Example of highlighter controller" width="389" height="159"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt;
  &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"highlighter"&lt;/span&gt;
  &lt;span class="na"&gt;data-highlighter-marker-value=&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;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:list_item&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="na"&gt;data-highlighter-highlight-class=&lt;/span&gt;&lt;span class="s"&gt;"text-blue-600 bg-blue-100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using both the Stimulus &lt;code&gt;values&lt;/code&gt; and &lt;code&gt;classes&lt;/code&gt; APIs, this controller is super reusable: we can specify any DOM element id and whatever classes we want to use to highlight the element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/highlighter_controller.js&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;Controller&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;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;highlight&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markedElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;highlightClasses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nx"&gt;markedElement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;markerValue&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;h3&gt;
  
  
  Grab focus
&lt;/h3&gt;

&lt;p&gt;We use a &lt;code&gt;grab-focus&lt;/code&gt; controller for a form where you can quickly add tasks. Submitting the form creates the task and then dynamically adds a new &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; for the next task. This controller seamlessly moves the browser focus to the new input.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aI0idMCF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://boringrails.com/images/grab-focus-example.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aI0idMCF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://boringrails.com/images/grab-focus-example.gif" alt="Example of grab-focus controller" width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/grab_focus_controller.js&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;Controller&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;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grabFocus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;grabFocus&lt;/span&gt;&lt;span class="p"&gt;()&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasSelectorValue&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectorValue&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;focus&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;h3&gt;
  
  
  Analytics “Beacons”
&lt;/h3&gt;

&lt;p&gt;We borrowed this idea from &lt;a href="https://www.hey.com/"&gt;HEY&lt;/a&gt; and use it for tracking page analytics. We add a &lt;code&gt;beacon&lt;/code&gt; to the page that pings the backend to record a page view and then removes itself.&lt;/p&gt;

&lt;p&gt;(If you’re fancy you could even use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API"&gt;Beacon Web API&lt;/a&gt;, but we’re justing sending an PATCH request here for simplicity!)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/beacon_controller.js&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;Controller&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;@hotwired/stimulus&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;patch&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;@rails/request.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urlValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&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;p&gt;We wrapped this one up in a Rails view helper for a more clean API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;AnalyticsHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tracking_beacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;template&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;controller: &lt;/span&gt;&lt;span class="s2"&gt;"beacon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;beacon_url_value: &lt;/span&gt;&lt;span class="n"&gt;url&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 class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Inside app/views/layouts/plan.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;tracking_beacon&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;plan_viewings_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@plan&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap it up
&lt;/h2&gt;

&lt;p&gt;Self-destructing Stimulus controllers are a great way to augment Hotwire applications by adding sprinkles of JavaScript behavior without having to completely eject and build the whole feature on the client-side. Keep them small and single-purpose and you’ll be able to reuse them across pages and in different contexts.&lt;/p&gt;

&lt;p&gt;Piggybacking on the existing lifecycle of Stimulus controllers ensures that things work as expected when changing content via Turbo Streams and navigating between pages with Turbo Drive.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0llgMsYy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/twitter-banner.png" alt="" width="722" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>hotwire</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>Tailwind style CSS transitions with StimulusJS</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Wed, 01 Jun 2022 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/tailwind-style-css-transitions-with-stimulusjs-4lem</link>
      <guid>https://forem.com/swanson/tailwind-style-css-transitions-with-stimulusjs-4lem</guid>
      <description>&lt;p&gt;This post is part of &lt;a href="https://dev.to/hotwire-summer"&gt;Hotwire Summer&lt;/a&gt;: a new season of content on Boring Rails!&lt;/p&gt;

&lt;p&gt;If you’ve built UI elements with StimulusJS before, you’ve certainly written code to show or hide an element on the page. Whether you are popping up a modal or sliding out a panel, we’ve all written controller code like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modalTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To take your UI designs to the next level, you can use transitions so elements don’t immediately appear or disappear from the screen. You can transition opacity to gently fade elements in and use translate to slide them into place.&lt;/p&gt;

&lt;p&gt;One issue trying to do these kind of animations with CSS &lt;code&gt;transition&lt;/code&gt; properties, as &lt;a href="https://twitter.com/sebdedeyne"&gt;Sebastian De Deyne&lt;/a&gt; lays out in this &lt;a href="https://sebastiandedeyne.com/javascript-framework-diet/enter-leave-transitions/"&gt;excellent primer on enter and leave transitions&lt;/a&gt;, is that you can’t change an element’s &lt;code&gt;display&lt;/code&gt; property before the transition occurs. We can use basic &lt;code&gt;transition&lt;/code&gt; styles for things like subtly changing a button color on hover, but for smooth animations when elements are shown or hidden, we need something more.&lt;/p&gt;

&lt;p&gt;One pattern that has emerged from the &lt;a href="https://vuejs.org/guide/built-ins/transition.html#css-based-transitions"&gt;Vue&lt;/a&gt; and &lt;a href="https://alpinejs.dev/directives/transition"&gt;Alpine&lt;/a&gt; communities is to use a series of &lt;code&gt;data&lt;/code&gt; attributes to define your desired CSS classes during the lifecycle of the transition. &lt;a href="https://tailwindui.com/components/application-ui/overlays/slide-overs"&gt;TailwindUI&lt;/a&gt; also follows this pattern when specifying how to animate their components. It has proven to be powerful and easy to understand.&lt;/p&gt;

&lt;p&gt;Each project has their own specific naming conventions, but they all define six basic lifecycle “stages”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entering: the classes that should be on an element during the whole time it is entering the page (sometimes called “Entering Active”, “Enter Active”, “Enter”, etc)&lt;/li&gt;
&lt;li&gt;Enter From: the starting point that you will transition from when entering the page (sometimes called “Enter Start”)&lt;/li&gt;
&lt;li&gt;Enter To: the ending point of the transition when entering the page (sometimes called “Enter End”)&lt;/li&gt;
&lt;li&gt;Leaving: the classes that should be on an element during the whole time it is leaving the page (sometimes called “Leaving Active”, “Leave Active”, “Leave”, etc)&lt;/li&gt;
&lt;li&gt;Leave From: the starting point that you will transition from when leaving the page (sometimes called “Leave Start”)&lt;/li&gt;
&lt;li&gt;Leave To: the ending point of the transition when leaving the page (sometimes called “Leave End”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This picture from the Vue docs really helped me visualize how it all works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L5-W6UaB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/enter-leave-diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L5-W6UaB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/enter-leave-diagram.png" alt="The lifecycle stages for CSS transitions in Vue" width="783" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The approach of using &lt;code&gt;data&lt;/code&gt; attributes works well with Tailwind’s transition utility classes and feels right at home with the StimulusJS philosophy of augmenting HTML markup.&lt;/p&gt;

&lt;p&gt;Vue (&lt;code&gt;Transition&lt;/code&gt;) and Alpine (&lt;code&gt;x-transition&lt;/code&gt;) provide native, first-party support for these transitions, but for a Rails app using Hotwire, we’ll have to add this functionality ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options
&lt;/h2&gt;

&lt;p&gt;I explored a few different options in this space. Here are the most popular approaches I came across:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;stimulus-transitions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/robbevp/stimulus-transition"&gt;This library&lt;/a&gt; provides a &lt;code&gt;transition&lt;/code&gt; controller that you can import and register. You use this controller like a normal Stimulus controller in your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"transition"&lt;/span&gt;
     &lt;span class="na"&gt;data-transition-enter-active=&lt;/span&gt;&lt;span class="s"&gt;"enter-class"&lt;/span&gt;
     &lt;span class="na"&gt;data-transition-enter-from=&lt;/span&gt;&lt;span class="s"&gt;"enter-from-class"&lt;/span&gt;
     &lt;span class="na"&gt;data-transition-enter-to=&lt;/span&gt;&lt;span class="s"&gt;"enter-to-class"&lt;/span&gt;
     &lt;span class="na"&gt;data-transition-leave-active=&lt;/span&gt;&lt;span class="s"&gt;"or-use multiple classes"&lt;/span&gt;
     &lt;span class="na"&gt;data-transition-leave-from=&lt;/span&gt;&lt;span class="s"&gt;"or-use multiple classes"&lt;/span&gt;
     &lt;span class="na"&gt;data-transition-leave-to=&lt;/span&gt;&lt;span class="s"&gt;"or-use multiple classes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- content --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller will automatically detect when the element is shown or hidden and run the transitions. There are also options for listening to custom &lt;code&gt;transition:end-enter&lt;/code&gt; and &lt;code&gt;transition:end-leave&lt;/code&gt; events if you want to run additional code when the transitions have finished.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;transition&lt;/code&gt; controller needs something to trigger the display style on the element so you will need an application-level controller to kick off the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;stimulus-use/useTransition&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;stimulus-use&lt;/code&gt; &lt;a href="https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-transition.md"&gt;project&lt;/a&gt; is a collection of reusable behaviors for Stimulus. If you are familiar with React, this project is similar to React’s &lt;code&gt;hooks&lt;/code&gt; system, but for Stimulus controllers.&lt;/p&gt;

&lt;p&gt;One particular mix-in available in this package is &lt;code&gt;useTransition&lt;/code&gt;. You can call this from your own Stimulus controller and it will run the transitions on the element (either reading from &lt;code&gt;data-&lt;/code&gt; attributes or you can specify the classes in JavaScript as options).&lt;/p&gt;

&lt;p&gt;This particular mix-in was flagged as a “beta” release at the time of this writing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;el-transition&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mmccall10/el-transition"&gt;This library&lt;/a&gt; is not Stimulus specific, but implements the same Vue/Alpine transition pattern. Since it is vanilla Javascript, there are no built-in hooks for Stimulus lifecycle or controllers to register. You import &lt;code&gt;enter&lt;/code&gt; and &lt;code&gt;leave&lt;/code&gt; functions directly and then call them while providing the element to transition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leave&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;el-transition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// in your stimulus controller somewhere&lt;/span&gt;
&lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modalTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modalTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole library is really just one, 60 line file so you can even just drop it into your project directly if you want to vendor it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My recommendation: el-transition
&lt;/h2&gt;

&lt;p&gt;All three libraries could do what I wanted: apply Vue/Alpine style &lt;code&gt;data-&lt;/code&gt; attribute transitions.&lt;/p&gt;

&lt;p&gt;I had the most success with &lt;code&gt;el-transition&lt;/code&gt; and selected it for my project.&lt;/p&gt;

&lt;p&gt;I liked that it was super simple and not tied to the framework. It has a minimal surface-area and I didn’t have to rely on an external library to update for newer Stimulus releases if there are breaking changes.&lt;/p&gt;

&lt;p&gt;One extra bonus was that &lt;code&gt;enter&lt;/code&gt; and &lt;code&gt;leave&lt;/code&gt; functions returned &lt;code&gt;Promise&lt;/code&gt; objects, which worked much better for coordinating multiple elements that need to transition (this is pretty common in the TailwindUI components).&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Tailwind UI Slide Over Menu
&lt;/h2&gt;

&lt;p&gt;Let’s put this advice into practice by building a slide-over menu from &lt;a href="https://tailwindui.com/"&gt;TailwindUI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Start by grabbing the HTML code template (we’re using one of the free samples for this article).&lt;/p&gt;

&lt;p&gt;In this case, we’ll create a &lt;code&gt;slide-over&lt;/code&gt; controller, with targets for the three parts in the Tailwind markup (&lt;code&gt;backdrop&lt;/code&gt;, &lt;code&gt;panel&lt;/code&gt;, and &lt;code&gt;closeButton&lt;/code&gt;) and then one more for the whole menu (&lt;code&gt;container&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Notice there are several code comments that highlight various parts of the component and how to transition them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!--
  Background backdrop, show/hide based on slide-over state.

  Entering: "ease-in-out duration-500"
    From: "opacity-0"
    To: "opacity-100"
  Leaving: "ease-in-out duration-500"
    From: "opacity-100"
    To: "opacity-0"
--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each of these component parts, we’re going to bind them as Stimulus targets and also add in the &lt;code&gt;data&lt;/code&gt; attributes to match the specification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"slide-over"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-slide-over-target=&lt;/span&gt;&lt;span class="s"&gt;"backdrop"&lt;/span&gt;
      &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-enter=&lt;/span&gt;&lt;span class="s"&gt;"ease-in-out duration-500"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-enter-start=&lt;/span&gt;&lt;span class="s"&gt;"opacity-0"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-enter-end=&lt;/span&gt;&lt;span class="s"&gt;"opacity-100"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-leave=&lt;/span&gt;&lt;span class="s"&gt;"ease-in-out duration-500"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-leave-start=&lt;/span&gt;&lt;span class="s"&gt;"opacity-100"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-leave-end=&lt;/span&gt;&lt;span class="s"&gt;"opacity-0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  ...
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat this for the other elements that we want to animate. We’ll also add a basic &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; to show the panel when clicked.&lt;/p&gt;

&lt;p&gt;Here is what the full markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"slide-over"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-input"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"slide-over#show"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Show slideover&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- This example requires Tailwind CSS v2.0+ --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-slide-over-target=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative z-10 hidden"&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby=&lt;/span&gt;&lt;span class="s"&gt;"slide-over-title"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt; &lt;span class="na"&gt;aria-modal=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--
      Background backdrop, show/hide based on slide-over state.

      Entering: "ease-in-out duration-500"
        From: "opacity-0"
        To: "opacity-100"
      Leaving: "ease-in-out duration-500"
        From: "opacity-100"
        To: "opacity-0"
    --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-slide-over-target=&lt;/span&gt;&lt;span class="s"&gt;"backdrop"&lt;/span&gt;
      &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-enter=&lt;/span&gt;&lt;span class="s"&gt;"ease-in-out duration-500"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-enter-start=&lt;/span&gt;&lt;span class="s"&gt;"opacity-0"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-enter-end=&lt;/span&gt;&lt;span class="s"&gt;"opacity-100"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-leave=&lt;/span&gt;&lt;span class="s"&gt;"ease-in-out duration-500"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-leave-start=&lt;/span&gt;&lt;span class="s"&gt;"opacity-100"&lt;/span&gt;
      &lt;span class="na"&gt;data-transition-leave-end=&lt;/span&gt;&lt;span class="s"&gt;"opacity-0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"fixed inset-0 overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute inset-0 overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"fixed inset-y-0 right-0 flex max-w-full pl-10 pointer-events-none"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="c"&gt;&amp;lt;!--
            Slide-over panel, show/hide based on slide-over state.

            Entering: "transform transition ease-in-out duration-500 sm:duration-700"
              From: "translate-x-full"
              To: "translate-x-0"
            Leaving: "transform transition ease-in-out duration-500 sm:duration-700"
              From: "translate-x-0"
              To: "translate-x-full"
          --&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-slide-over-target=&lt;/span&gt;&lt;span class="s"&gt;"panel"&lt;/span&gt;
            &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative w-screen max-w-md pointer-events-auto"&lt;/span&gt;
            &lt;span class="na"&gt;data-transition-enter=&lt;/span&gt;&lt;span class="s"&gt;"transform transition ease-in-out duration-500 sm:duration-700"&lt;/span&gt;
            &lt;span class="na"&gt;data-transition-enter-start=&lt;/span&gt;&lt;span class="s"&gt;"translate-x-full"&lt;/span&gt;
            &lt;span class="na"&gt;data-transition-enter-end=&lt;/span&gt;&lt;span class="s"&gt;"translate-x-0"&lt;/span&gt;
            &lt;span class="na"&gt;data-transition-leave=&lt;/span&gt;&lt;span class="s"&gt;"transform transition ease-in-out duration-500 sm:duration-700"&lt;/span&gt;
            &lt;span class="na"&gt;data-transition-leave-start=&lt;/span&gt;&lt;span class="s"&gt;"translate-x-0"&lt;/span&gt;
            &lt;span class="na"&gt;data-transition-leave-end=&lt;/span&gt;&lt;span class="s"&gt;"translate-x-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!--
              Close button, show/hide based on slide-over state.

              Entering: "ease-in-out duration-500"
                From: "opacity-0"
                To: "opacity-100"
              Leaving: "ease-in-out duration-500"
                From: "opacity-100"
                To: "opacity-0"
            --&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-slide-over-target=&lt;/span&gt;&lt;span class="s"&gt;"closeButton"&lt;/span&gt;
              &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sm:-ml-10 sm:pr-4 absolute top-0 left-0 flex pt-4 pr-2 -ml-8"&lt;/span&gt;
              &lt;span class="na"&gt;data-transition-enter=&lt;/span&gt;&lt;span class="s"&gt;"ease-in-out duration-500"&lt;/span&gt;
              &lt;span class="na"&gt;data-transition-enter-start=&lt;/span&gt;&lt;span class="s"&gt;"opacity-0"&lt;/span&gt;
              &lt;span class="na"&gt;data-transition-enter-end=&lt;/span&gt;&lt;span class="s"&gt;"opacity-100"&lt;/span&gt;
              &lt;span class="na"&gt;data-transition-leave=&lt;/span&gt;&lt;span class="s"&gt;"ease-in-out duration-500"&lt;/span&gt;
              &lt;span class="na"&gt;data-transition-leave-start=&lt;/span&gt;&lt;span class="s"&gt;"opacity-100"&lt;/span&gt;
              &lt;span class="na"&gt;data-transition-leave-end=&lt;/span&gt;&lt;span class="s"&gt;"opacity-0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"slide-over#hide"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hover:text-white focus:outline-none focus:ring-2 focus:ring-white text-gray-300 rounded-md"&lt;/span&gt;&lt;span class="nt"&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;"sr-only"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close panel&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
                &lt;span class="c"&gt;&amp;lt;!-- Heroicon name: outline/x --&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-6 h-6"&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M6 18L18 6M6 6l12 12"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col h-full py-6 overflow-y-auto bg-white shadow-xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sm:px-6 px-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-lg font-medium text-gray-900"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"slide-over-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Panel title&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sm:px-6 relative flex-1 px-4 mt-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="c"&gt;&amp;lt;!-- Replace with your content --&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sm:px-6 absolute inset-0 px-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-full border-2 border-gray-200 border-dashed"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    Your content goes here! How about a lazy-loaded turbo-frame?
                  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                &lt;span class="c"&gt;&amp;lt;!-- /End replace --&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we need to actually implement the Stimulus controller to run the transitions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&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="s2"&gt;@hotwired/stimulus&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;enter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leave&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;el-transition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backdrop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;closeButton&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backdropTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closeButtonTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;enter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;panelTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;hide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backdropTarget&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closeButtonTarget&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;panelTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the &lt;code&gt;show&lt;/code&gt; action is called (by clicking the button), we remove the &lt;code&gt;hidden&lt;/code&gt; class on the whole container and then run the &lt;code&gt;enter&lt;/code&gt; function from &lt;code&gt;el-transition&lt;/code&gt; on each of the targets we want to animate. This will fade in the backdrop and close button and slide over the panel using the Tailwind classes we defined in the &lt;code&gt;data&lt;/code&gt; attributes.&lt;/p&gt;

&lt;p&gt;When we trigger the &lt;code&gt;hide&lt;/code&gt; action (by clicking the close button), we do everything in reverse. We run the &lt;code&gt;leave&lt;/code&gt; function and the panel slides back over and the backdrop and close button fade away. Once all the transitions are done, we hide the whole container. By using &lt;code&gt;Promise.all&lt;/code&gt; we can wait all of the individual transitions to finish (remember they may have different durations!) before hiding the container.&lt;/p&gt;

&lt;p&gt;No need for &lt;code&gt;setTimeout&lt;/code&gt; or flashing of content when the transition is finished and then removed!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NT7cSG00--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://boringrails.com/images/slide-over-menu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NT7cSG00--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://boringrails.com/images/slide-over-menu.gif" alt="Example video of StimulusJS slide-over menu from TailwindUI" width="880" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s not quite as convenient as dropping in the React or Vue snippets from TailwindUI, but it’s pretty close!&lt;/p&gt;

&lt;p&gt;You may want to take this a step further by extracting the markup into a partial or use ViewComponent to clean up the code, but that is left as an exercise for the reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap it up
&lt;/h2&gt;

&lt;p&gt;It’s great to follow other front-end communities and bring back ideas into your own ecosystem. Vue and Alpine have established a really clear, understandable pattern for specifying CSS transitions and we can leverage that work in a StimulusJS/Hotwire project with a small library.&lt;/p&gt;

&lt;p&gt;These transitions take a bit of time to wrap your head around, but they add a nice level of polish to your UI components with minimal effort: exactly the kind of high leverage techniques we want when building apps in the “boring Rails” style.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0llgMsYy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/twitter-banner.png" alt="" width="722" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>hotwire</category>
      <category>stimulus</category>
    </item>
    <item>
      <title>Ordinalize: Adding "-st", "-nd", "-rd", "-th" to dates</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Wed, 29 Sep 2021 16:44:29 +0000</pubDate>
      <link>https://forem.com/swanson/ordinalize-adding-st-nd-rd-th-to-dates-12am</link>
      <guid>https://forem.com/swanson/ordinalize-adding-st-nd-rd-th-to-dates-12am</guid>
      <description>&lt;p&gt;Ever written a giant case statement to add "-st", "-nd", "-rd", "-th" to display dates like "March 7th" or "September 1st"?&lt;/p&gt;

&lt;p&gt;Converting numbers like 1, 2, 7, 19 into "1st", "2nd", "7th", "19th" is called ordinalizing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ordinalize(number)&lt;br&gt;
Turns a number into an ordinal string used to denote the position in an ordered sequence such as 1st, 2nd, 3rd, 4th.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of manually add special cases, check if your language, library, or framework has a helper built-in.&lt;/p&gt;

&lt;p&gt;For example, in Rails, you can call the &lt;code&gt;ordinalize&lt;/code&gt; method on a number or use the &lt;code&gt;long_ordinal&lt;/code&gt; date format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ordinalize&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"3rd"&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:long_ordinal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"September 29th, 2021"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy hacking!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>javascript</category>
      <category>programming</category>
      <category>todayilearned</category>
    </item>
    <item>
      <title>Debugging slow Heroku builds</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Mon, 17 May 2021 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/debugging-slow-heroku-builds-5df8</link>
      <guid>https://forem.com/swanson/debugging-slow-heroku-builds-5df8</guid>
      <description>&lt;p&gt;It’s always best to follow a systematic approach when trying to speed up slow code.&lt;/p&gt;

&lt;p&gt;First, measure the current performance.&lt;/p&gt;

&lt;p&gt;Next, make the change that you think will help.&lt;/p&gt;

&lt;p&gt;Lastly, measure again to see if the change worked.&lt;/p&gt;

&lt;p&gt;It’s no different when it comes to debugging slow test suites or deploys.&lt;/p&gt;

&lt;p&gt;I recently noticed that my Heroku deploys were taking nearly 10 minutes to build.&lt;/p&gt;

&lt;p&gt;Thanks to a tip from &lt;a href="https://twitter.com/panozzaj" rel="noopener noreferrer"&gt;my friend Anthony&lt;/a&gt;, I was able to quickly measure, diagnose, and deploy a fix that shaved nearly 4 minutes off every Heroku build. Big win!&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Heroku uses “buildpacks”, which are essentially shell scripts that run to install and configure your server.&lt;/p&gt;

&lt;p&gt;You can add multiple buildpacks: it’s common for a Ruby on Rails app to have a Ruby buildpack (for running the Rails app) and a Node buildpack (for building Javascript assets).&lt;/p&gt;

&lt;p&gt;But even if you watch the build log as your code changes deploy, there aren’t many affordances for knowing what parts of the process are slow.&lt;/p&gt;

&lt;p&gt;Enter the &lt;code&gt;heroku-buildpack-timestamps&lt;/code&gt; buildpack!&lt;/p&gt;

&lt;p&gt;You can add this &lt;a href="https://elements.heroku.com/buildpacks/edmorley/heroku-buildpack-timestamps" rel="noopener noreferrer"&gt;buildpack&lt;/a&gt; to your Heroku app and it will output timestamps of each step in the process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2021-05-10 19:30:59
2021-05-10 19:30:59 -----&amp;gt; Creating runtime environment
2021-05-10 19:30:59
2021-05-10 19:30:59 NPM_CONFIG_LOGLEVEL=error
2021-05-10 19:30:59 YARN_PRODUCTION=true
2021-05-10 19:30:59 NODE_ENV=production
2021-05-10 19:30:59 NODE_MODULES_CACHE=true
2021-05-10 19:30:59 NODE_VERBOSE=false
2021-05-10 19:30:59
2021-05-10 19:30:59 -----&amp;gt; Installing binaries
2021-05-10 19:30:59 engines.node (package.json): unspecified (use default)
2021-05-10 19:30:59 engines.npm (package.json): unspecified (use default)
2021-05-10 19:30:59 engines.yarn (package.json): unspecified (use default)
2021-05-10 19:30:59
2021-05-10 19:30:59 Resolving node version 14.x...
2021-05-10 19:31:00 Downloading and installing node 14.17.0...
2021-05-10 19:31:01 Using default npm version: 6.14.13
2021-05-10 19:31:01 Resolving yarn version 1.22.x...
2021-05-10 19:31:01 Downloading and installing yarn (1.22.10)
2021-05-10 19:31:02 Installed yarn 1.22.10
2021-05-10 19:31:03
2021-05-10 19:31:03 -----&amp;gt; Restoring cache
2021-05-10 19:31:03 - node_modules
2021-05-10 19:31:05
2021-05-10 19:31:05 -----&amp;gt; Building dependencies
2021-05-10 19:31:06 Installing node modules (yarn.lock)
2021-05-10 19:31:07 yarn install v1.22.10
2021-05-10 19:34:10 Done in 191.01s.
2021-05-10 19:34:10
2021-05-10 19:34:10 -----&amp;gt; Caching build
2021-05-10 19:34:10 - node_modules
2021-05-10 19:34:17
2021-05-10 19:34:17 -----&amp;gt; Pruning devDependencies
2021-05-10 19:34:17 Skipping because YARN_PRODUCTION is 'true'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added this buildpack to my staging Heroku app and was able to pinpoint the hot spots in the process. My findings showed that running &lt;code&gt;yarn install&lt;/code&gt; was taking 4 minutes, even when there were no Javascript changes between builds.&lt;/p&gt;

&lt;p&gt;After identifying the troublesome build step, I decided to turn on the &lt;code&gt;--verbose&lt;/code&gt; flag for &lt;code&gt;yarn&lt;/code&gt; to see why things were so slow. The official &lt;code&gt;nodejs&lt;/code&gt; buildpack is supposed to automatically cache &lt;code&gt;npm&lt;/code&gt; packages so it was strange that every build was spending 4 minutes installing.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--verbose&lt;/code&gt; flag generated a huge amount of extra detail to help me find the problem – over 23k lines of logging…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Downloading binary from https://github.com/sass/node-sass/releases/download/v4.13.1/linux-x64-83_binding.node
Cannot download "https://github.com/sass/node-sass/releases/download/v4.13.1/linux-x64-83_binding.node":

HTTP error 404 Not Found

Hint: If github.com is not accessible in your location
      try setting a proxy via HTTP_PROXY, e.g.

      export HTTP_PROXY=http://example.com:1234

or configure npm proxy via

      npm config set proxy http://example.com:8080

Building: /tmp/build_496d129f/.heroku/node/bin/node /tmp/build_496d129f/node_modules/node-gyp/bin/node-gyp.js rebuild --verbose --libsass_ext= --libsass_cflags= --libsass_ldflags= --libsass_library=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buried deep in the logs was the culprit. We were trying to download a pre-compiled &lt;code&gt;node-sass&lt;/code&gt; binary, but it wasn’t found (because Heroku was running Node 14 and the version we asked for was only built for Node 10). After timing out, we then built the native package manually. Aha!&lt;/p&gt;

&lt;p&gt;(The fix was to update &lt;code&gt;webpacker&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;While this fix was specific to my project, the process is generalizable: measure your Heroku build to find the slow parts, then dig deeper until you can find the underlying problem.&lt;/p&gt;

&lt;p&gt;If you find yourself waiting and waiting for your Heroku builds to finish, try adding this buildpack so you can debug why it’s slow.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fboringrails.com%2Fimages%2Ftwitter-banner.png"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>heroku</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Quickly explore your data with `uniq` and `tally`</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Wed, 05 May 2021 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/quickly-explore-your-data-with-uniq-and-tally-4iaj</link>
      <guid>https://forem.com/swanson/quickly-explore-your-data-with-uniq-and-tally-4iaj</guid>
      <description>&lt;p&gt;A common question you may want to answer on user-input data is: what values have been entered and how many times is each one used?&lt;/p&gt;

&lt;p&gt;Maybe you have a list of dropdown options and you want to investigate removing a rare-used option.&lt;/p&gt;

&lt;p&gt;Ruby has two handy methods that I reach for often: &lt;code&gt;uniq&lt;/code&gt; and &lt;code&gt;tally&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;uniq&lt;/code&gt; method operates on an enumerable and compresses your data down to unique values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Outreach&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;uniq&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Confirmed w/o Outreach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Awaiting Outreach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Responded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"No Response Expected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Follow-up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Awaiting Reply"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While most developers are familiar with &lt;code&gt;uniq&lt;/code&gt;, the &lt;code&gt;tally&lt;/code&gt; method is one of the best kept secrets in Ruby. The &lt;code&gt;tally&lt;/code&gt; method takes an enumerable of values and returns a hash where the keys are unique values and the values are the number of times the value appeared in the list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Outreach&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tally&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Confirmed w/o Outreach"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;106&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Awaiting Outreach"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Responded"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"No Response Expected"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Follow-up"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s2"&gt;"Awaiting Reply"&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two methods are great to have in your toolbox to quickly explore your data in a Rails console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;p&gt;Ruby API: &lt;a href="https://ruby-doc.org/core-3.0.0/Enumerable.html#method-i-uniq"&gt;Enumerable#uniq&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ruby API: &lt;a href="https://ruby-doc.org/core-3.0.0/Enumerable.html#method-i-tally"&gt;Enumerable#tally&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0llgMsYy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/twitter-banner.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Improving your Rails mailers with `email_address_with_name`</title>
      <dc:creator>matt swanson</dc:creator>
      <pubDate>Mon, 03 May 2021 13:00:00 +0000</pubDate>
      <link>https://forem.com/swanson/improving-your-rails-mailers-with-emailaddresswithname-561k</link>
      <guid>https://forem.com/swanson/improving-your-rails-mailers-with-emailaddresswithname-561k</guid>
      <description>&lt;p&gt;In almost all email programs, you can add a display name before your email address like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;To: Matt Swanson &amp;lt;matt@example.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s a small touch, but it is a more human-readable way of addressing an email. Rails provides a helper utility to format email addresses in this style without resorting to manual string manipulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;email_address_with_name&lt;/code&gt; to add a name in-front on an email address in a standard way&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActionMailer&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"swan3788@gmail.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Matt Swanson"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Matt Swanson &amp;lt;swan3788@gmail.com&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helper is available in all Rails mailers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserMailer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="ss"&gt;from: &lt;/span&gt;&lt;span class="s1"&gt;'notifications@example.com'&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;welcome_email&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&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;:user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="n"&gt;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;subject: &lt;/span&gt;&lt;span class="s1"&gt;'You have a new message'&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;h2&gt;
  
  
  Options
&lt;/h2&gt;

&lt;p&gt;This helper handles &lt;code&gt;nil&lt;/code&gt; gracefully as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActionMailer&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"swan3788@gmail.com"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"swan3788@gmail.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it handles escaping characters automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ActionMailer&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"mike@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Michael J. Scott"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Michael J. Scott&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;mike@example.com&amp;gt;"&lt;/span&gt;

&lt;span class="no"&gt;ActionMailer&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;email_address_with_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"chip@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'John "Chip" Smith'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;John &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;Chip&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; Smith&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;chip@example.com&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;p&gt;Rails API: &lt;a href="https://api.rubyonrails.org/classes/ActionMailer/Base.html#method-c-email_address_with_name"&gt;ActionMailer::Base#email_address_with_name&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://twitter.com/_swanson"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0llgMsYy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://boringrails.com/images/twitter-banner.png" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
