<?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom" >
  <generator uri="https://jekyllrb.com/" version="3.8.7">Jekyll</generator>
  <link href="https://boringrails.com/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://boringrails.com/" rel="alternate" type="text/html" />
  <updated>2026-03-14T13:29:50+00:00</updated>
  <id>https://boringrails.com/feed.xml</id>

  
  
  

  
    <title type="html">Boring Rails: Skip the bullshit and ship fast | </title>
  

  
    <subtitle>Learn about the boring tools and practices used by Basecamp, GitHub, and Shopify to keep you as happy and productive as the day you typed rails new</subtitle>
  

  
    <author>
        <name>Matt Swanson</name>
      
      
    </author>
  

  
  
  

  
    
    

    
      <entry>
        

        <title type="html">Beautiful Rails confirmation dialogs (with zero JavaScript)</title>
        <link href="https://boringrails.com/articles/data-turbo-confirm-beautiful-dialog/" rel="alternate" type="text/html" title="Beautiful Rails confirmation dialogs (with zero JavaScript)" />
        <published>2025-12-15T13:00:00+00:00</published>
        <updated>2025-12-15T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/data-turbo-confirm-beautiful-dialog</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/data-turbo-confirm-beautiful-dialog/">&lt;p class=&quot;guest-intro&quot;&gt;This is a guest collaboration with &lt;a href=&quot;https://x.com/fractaledmind&quot;&gt;Stephen Margheim&lt;/a&gt;, creator of &lt;a href=&quot;https://highleveragerails.com/?utm_source=boring-rails&quot;&gt;High Leverage Rails&lt;/a&gt;, a video course on building high-quality Rails applications with the power and simplicity of SQLite, HTML, and CSS.&lt;/p&gt;

&lt;p&gt;Turbo’s &lt;code class=&quot;highlighter-rouge&quot;&gt;data-turbo-confirm&lt;/code&gt; attribute is convenient for quick confirmation dialogs, but the native &lt;code class=&quot;highlighter-rouge&quot;&gt;confirm()&lt;/code&gt; prompt it triggers looks dated and out of place. If you want a styled confirmation dialog that matches your app’s design, the &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive#requiring-confirmation-for-a-visit&quot;&gt;traditional&lt;/a&gt; &lt;a href=&quot;https://gorails.com/episodes/custom-hotwire-turbo-confirm-modals&quot;&gt;approach&lt;/a&gt; &lt;a href=&quot;https://www.beflagrant.com/blog/turbo-confirmation-bias-2024-01-10&quot;&gt;recommends&lt;/a&gt; a lot of JavaScript — a Stimulus controller to open and close the dialog, event listeners for keyboard handling, and coordination between the trigger and the modal.&lt;/p&gt;

&lt;p&gt;But, recent browser updates have changed the game. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API&quot;&gt;Invoker Commands&lt;/a&gt; landed in Chrome 131 and Safari 18.4, giving us declarative dialog control. Combined with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;@starting-style&lt;/code&gt;&lt;/a&gt; for animations, we can now build beautiful, animated confirmation dialogs without writing any JavaScript.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;How…&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Open dialog&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;command=&quot;show-modal&quot;&lt;/code&gt; on a button&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Close dialog&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;command=&quot;close&quot;&lt;/code&gt; on cancel button&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Escape key&lt;/td&gt;
      &lt;td&gt;Built-in browser behavior&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Light dismiss&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;closedby=&quot;any&quot;&lt;/code&gt; attribute&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Enter animation&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;@starting-style&lt;/code&gt; CSS rule&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Exit animation&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;allow-discrete&lt;/code&gt; on &lt;code class=&quot;highlighter-rouge&quot;&gt;display&lt;/code&gt; transition&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Let’s imagine we wanted a confirmation dialog for when a user decides to delete an item in our app. Here is how we could build such a dialog today using modern browser features with zero JavaScript:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;commandfor=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;delete-item-dialog&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;command=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;show-modal&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  Delete this item
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;delete-item-dialog&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;closedby=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;any&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;alertdialog&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;aria-labelledby=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dialog-title&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;aria-describedby=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dialog-desc&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;hgroup&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;h3&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dialog-title&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Delete this item?&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dialog-desc&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Are you sure you want to permanently delete this item?&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/hgroup&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;commandfor=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;delete-item-dialog&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;command=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;close&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      Cancel
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;button_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      Delete item
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#command&quot;&gt;command&lt;/a&gt; attribute tells the browser what action to perform, and &lt;code class=&quot;highlighter-rouge&quot;&gt;commandfor&lt;/code&gt; specifies the target element by &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt;. With &lt;code class=&quot;highlighter-rouge&quot;&gt;command=&quot;show-modal&quot;&lt;/code&gt;, clicking the button calls &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal&quot;&gt;showModal()&lt;/a&gt; on the target dialog. The cancel button uses &lt;code class=&quot;highlighter-rouge&quot;&gt;command=&quot;close&quot;&lt;/code&gt; to call the dialog’s &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close&quot;&gt;close() method&lt;/a&gt;. Note the cancel button is &lt;code class=&quot;highlighter-rouge&quot;&gt;type=&quot;button&quot;&lt;/code&gt;, not &lt;code class=&quot;highlighter-rouge&quot;&gt;type=&quot;submit&quot;&lt;/code&gt; — we don’t want it participating in any form submission.&lt;/p&gt;

&lt;p&gt;Modal dialogs opened with &lt;code class=&quot;highlighter-rouge&quot;&gt;showModal()&lt;/code&gt; automatically close on Escape. The browser handles it. Adding &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog#closedby&quot;&gt;closedby=”any”&lt;/a&gt; enables “light dismiss” — clicking the backdrop closes the dialog too.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;aria-labelledby&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;aria-describedby&lt;/code&gt; attributes connect the dialog to its heading and description. Screen reader users hear both the title and explanatory text announced immediately, giving them full context before making a decision. And &lt;code class=&quot;highlighter-rouge&quot;&gt;role=&quot;alertdialog&quot;&lt;/code&gt;	signals the dialog is a confirmation window communicating an important message that requires a user response.&lt;/p&gt;

&lt;h2 id=&quot;interactive-demo&quot;&gt;Interactive demo&lt;/h2&gt;

&lt;p&gt;Here’s a working demo showing how different close mechanisms work. Try clicking “Delete Item”, then close it different ways: click Cancel, click Delete, press Escape, or click the backdrop. Notice how &lt;code class=&quot;highlighter-rouge&quot;&gt;returnValue&lt;/code&gt; is only &lt;code class=&quot;highlighter-rouge&quot;&gt;&quot;confirm&quot;&lt;/code&gt; when you click the Delete button.&lt;/p&gt;

&lt;style&gt;
  /* Demo container */
  .demo-container {
    overflow: hidden;
    border-radius: 0.375rem;
    background-color: #27272a;
    margin-bottom: 1.5rem;
  }
  .demo-trigger-area {
    padding: 1.5rem;
  }

  /* Trigger button */
  .demo-trigger-btn {
    display: block;
    margin-left: auto;
    margin-right: auto;
    border-radius: 0.375rem;
    background-color: rgb(239 68 68 / 0.2);
    padding: 0.5rem 0.75rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: #f87171;
    border: none;
    cursor: pointer;
  }
  .demo-trigger-btn:hover {
    background-color: rgb(239 68 68 / 0.3);
  }

  /* Dialog */
  #demo-confirm-dialog {
    border-radius: 0.5rem;
    background-color: #27272a;
    padding: 1.5rem;
    text-align: left;
    box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
    border: none;
    outline: 1px solid rgb(255 255 255 / 0.1);
    outline-offset: -1px;
    margin-top: 33dvh;
    width: 100%;
    max-width: 32rem;

    /* Animation */
    opacity: 1;
    scale: 1;
    transition: opacity 0.2s ease-out, scale 0.2s ease-out,
      overlay 0.2s ease-out allow-discrete, display 0.2s ease-out allow-discrete;
  }
  #demo-confirm-dialog[open] {
    @starting-style {
      opacity: 0;
      scale: 0.95;
    }
  }
  #demo-confirm-dialog:not([open]) {
    opacity: 0;
    scale: 0.95;
  }
  #demo-confirm-dialog::backdrop {
    background-color: rgb(0 0 0 / 0.5);
    transition: background-color 0.2s ease-out,
      overlay 0.2s ease-out allow-discrete, display 0.2s ease-out allow-discrete;
  }
  #demo-confirm-dialog[open]::backdrop {
    @starting-style {
      background-color: rgb(0 0 0 / 0);
    }
  }
  #demo-confirm-dialog:not([open])::backdrop {
    background-color: rgb(0 0 0 / 0);
  }

  /* Dialog header */
  #demo-confirm-dialog header hgroup {
    text-align: left;
    margin: 0;
  }
  #demo-dialog-title {
    font-size: 1rem;
    font-weight: 600;
    color: white;
    margin: 0;
  }
  #demo-dialog-desc {
    margin: 0.5rem 0 0 0;
    font-size: 0.875rem;
    color: #a1a1aa;
  }

  /* Dialog footer */
  #demo-confirm-dialog footer {
    margin-top: 1rem;
    display: flex;
    flex-direction: row;
    gap: 0.75rem;
  }
  #demo-confirm-dialog footer form {
    margin: 0;
  }

  /* Cancel button */
  .demo-cancel-btn {
    display: inline-flex;
    justify-content: center;
    border-radius: 0.375rem;
    background-color: rgb(255 255 255 / 0.1);
    padding: 0.5rem 0.75rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: white;
    box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.05);
    border: none;
    cursor: pointer;
  }
  .demo-cancel-btn:hover {
    background-color: rgb(255 255 255 / 0.2);
  }

  /* Delete/Confirm button */
  .demo-confirm-btn {
    display: inline-flex;
    justify-content: center;
    border-radius: 0.375rem;
    background-color: #ef4444;
    padding: 0.5rem 0.75rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: white;
    border: none;
    cursor: pointer;
  }
  .demo-confirm-btn:hover {
    background-color: #f87171;
  }

  /* Event log area */
  .demo-log-area {
    margin-top: 1rem;
    overflow: hidden;
    border-top: 1px solid #3f3f46;
  }
  .demo-log-title {
    margin: 0;
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: white;
  }
  #demo-log {
    display: block;
    max-height: 10rem;
    min-height: 5rem;
    overflow-y: auto;
    padding: 1rem;
    scrollbar-color: #3f3f46 transparent;
    scrollbar-width: thin;
  }
  #demo-log-list {
    margin: 0;
    list-style: none;
    padding: 0;
  }
  #demo-log-list li {
    position: relative;
    display: flex;
    gap: 1rem;
  }
  #demo-log-list li + li {
    margin-top: 0.5rem;
  }
  #demo-log-list p {
    flex: 1 1 auto;
    padding: 0.125rem 0;
    font-size: 0.75rem;
    line-height: 1.25rem;
    color: #a1a1aa;
    margin: 0;
  }
  #demo-log-list time {
    flex: none;
    padding: 0.125rem 0;
    font-size: 0.75rem;
    line-height: 1.25rem;
    color: #a1a1aa;
  }
  #demo-log-list .log-label {
    font-weight: 500;
    color: white;
  }
  #demo-log-list code {
    border-radius: 0.25rem;
    background-color: rgb(255 255 255 / 0.1);
    padding: 0.125rem 0.375rem;
    color: #60a5fa;
  }
&lt;/style&gt;

&lt;script src=&quot;https://unpkg.com/invokers-polyfill&quot; type=&quot;module&quot;&gt;&lt;/script&gt;

&lt;script src=&quot;https://unpkg.com/@fractaledmind/dialog-closedby-polyfill&quot; type=&quot;module&quot;&gt;&lt;/script&gt;

&lt;div class=&quot;demo-container not-prose&quot;&gt;
  &lt;div class=&quot;demo-trigger-area&quot;&gt;
    &lt;button class=&quot;demo-trigger-btn&quot; commandfor=&quot;demo-confirm-dialog&quot; command=&quot;show-modal&quot;&gt;
      Delete Item
    &lt;/button&gt;
    &lt;dialog id=&quot;demo-confirm-dialog&quot; closedby=&quot;any&quot; aria-labelledby=&quot;demo-dialog-title&quot; aria-describedby=&quot;demo-dialog-desc&quot;&gt;
      &lt;header&gt;
        &lt;hgroup&gt;
          &lt;h3 id=&quot;demo-dialog-title&quot;&gt;Delete this item?&lt;/h3&gt;
          &lt;p id=&quot;demo-dialog-desc&quot;&gt;This action cannot be undone.&lt;/p&gt;
        &lt;/hgroup&gt;
      &lt;/header&gt;
      &lt;footer&gt;
        &lt;button type=&quot;button&quot; class=&quot;demo-cancel-btn&quot; commandfor=&quot;demo-confirm-dialog&quot; command=&quot;close&quot; autofocus=&quot;&quot;&gt;
          Cancel
        &lt;/button&gt;
        &lt;form method=&quot;dialog&quot;&gt;
          &lt;button type=&quot;submit&quot; class=&quot;demo-confirm-btn&quot; value=&quot;confirm&quot;&gt;
            Delete
          &lt;/button&gt;
        &lt;/form&gt;
      &lt;/footer&gt;
    &lt;/dialog&gt;
  &lt;/div&gt;

  &lt;div class=&quot;demo-log-area&quot;&gt;
    &lt;p class=&quot;demo-log-title&quot;&gt;Event Log&lt;/p&gt;
    &lt;output id=&quot;demo-log&quot; aria-live=&quot;polite&quot;&gt;
      &lt;ul role=&quot;list&quot; id=&quot;demo-log-list&quot;&gt;&lt;/ul&gt;
    &lt;/output&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
  (function () {
    const dialog = document.getElementById(&quot;demo-confirm-dialog&quot;);
    const log = document.getElementById(&quot;demo-log-list&quot;);
    if (!dialog || !log) return;

    function logEvent(message) {
      const time = new Date().toLocaleTimeString();
      const entry = document.createElement(&quot;li&quot;);
      entry.innerHTML = `
        &lt;p&gt;${message}&lt;/p&gt;
        &lt;time&gt;${time}&lt;/time&gt;
      `;
      log.prepend(entry);
    }

    dialog.addEventListener(&quot;toggle&quot;, function (event) {
      console.log(event);
      if (event.newState === &quot;open&quot;) {
        dialog.returnValue = &quot;&quot;;
        logEvent(
          &apos;&lt;span class=&quot;log-label&quot;&gt;toggle&lt;/span&gt; event — newState: &lt;code&gt;open&lt;/code&gt;&apos;
        );
      } else {
        logEvent(
          &apos;&lt;span class=&quot;log-label&quot;&gt;toggle&lt;/span&gt; event — newState: &lt;code&gt;closed&lt;/code&gt;&apos;
        );
      }
    });

    dialog.addEventListener(&quot;close&quot;, function () {
      const returnValue = dialog.returnValue || &quot;(empty)&quot;;
      logEvent(
        &apos;&lt;span class=&quot;log-label&quot;&gt;close&lt;/span&gt; event — returnValue: &lt;code&gt;&apos; +
          returnValue +
          &quot;&lt;/code&gt;&quot;
      );
    });

    dialog.addEventListener(&quot;cancel&quot;, function () {
      logEvent(&apos;&lt;span class=&quot;log-label&quot;&gt;cancel&lt;/span&gt; event (Escape key)&apos;);
    });
  })();
&lt;/script&gt;

&lt;p&gt;So much functionality with nothing but declarative HTML! I love it.&lt;/p&gt;

&lt;h2 id=&quot;adding-animations-with-starting-style&quot;&gt;Adding Animations with @starting-style&lt;/h2&gt;

&lt;p&gt;For a polished feel, add smooth enter/exit transitions. With &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style&quot;&gt;@starting-style&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior&quot;&gt;allow-discrete&lt;/a&gt;, we can animate dialogs purely in CSS.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;@starting-style&lt;/code&gt; rule defines the initial state when an element first appears. Without it, the browser renders the dialog immediately in its final state. With it, the browser starts from &lt;code class=&quot;highlighter-rouge&quot;&gt;opacity: 0; scale: 0.95&lt;/code&gt; and transitions to &lt;code class=&quot;highlighter-rouge&quot;&gt;opacity: 1; scale: 1&lt;/code&gt;, for example.&lt;/p&gt;

&lt;p&gt;For exit animations, we need &lt;code class=&quot;highlighter-rouge&quot;&gt;transition-behavior: allow-discrete&lt;/code&gt; on &lt;code class=&quot;highlighter-rouge&quot;&gt;display&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;overlay&lt;/code&gt;. Most CSS properties are continuous — opacity can be 0.5, colors can blend. But &lt;code class=&quot;highlighter-rouge&quot;&gt;display&lt;/code&gt; is discrete: it’s either &lt;code class=&quot;highlighter-rouge&quot;&gt;none&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;block&lt;/code&gt;, with no intermediate values. Historically, this meant &lt;code class=&quot;highlighter-rouge&quot;&gt;display&lt;/code&gt; changes couldn’t animate.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;allow-discrete&lt;/code&gt; keyword tells the browser to apply transition timing even for discrete properties. For closing animations, the browser keeps the element visible, runs the exit transition, then flips to &lt;code class=&quot;highlighter-rouge&quot;&gt;display: none&lt;/code&gt; only after the transition completes. The &lt;code class=&quot;highlighter-rouge&quot;&gt;overlay&lt;/code&gt; property works similarly — it controls whether the dialog stays in the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Top_layer&quot;&gt;top layer&lt;/a&gt; during the transition.&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;dialog&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;nl&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;opacity&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ease-out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;scale&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ease-out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;overlay&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ease-out&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allow-discrete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;display&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ease-out&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allow-discrete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;err&quot;&gt;@starting-style&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.95&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;:not&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.95&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::backdrop&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;rgb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;background-color&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ease-out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;overlay&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ease-out&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allow-discrete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;display&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2s&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ease-out&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;allow-discrete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;err&quot;&gt;@starting-style&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;rgb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;:not&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::backdrop&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;rgb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;browser-support&quot;&gt;Browser Support&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;Chrome&lt;/th&gt;
      &lt;th&gt;Safari&lt;/th&gt;
      &lt;th&gt;Firefox&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;Can I Use?&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;command&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;135+&lt;/td&gt;
      &lt;td&gt;26.2+&lt;/td&gt;
      &lt;td&gt;144+&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;https://caniuse.com/mdn-api_htmlbuttonelement_command&quot;&gt;link&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;commandfor&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;135+&lt;/td&gt;
      &lt;td&gt;26.2+&lt;/td&gt;
      &lt;td&gt;144+&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;https://caniuse.com/mdn-html_elements_button_commandfor&quot;&gt;link&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;@starting-style&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;117+&lt;/td&gt;
      &lt;td&gt;17.5+&lt;/td&gt;
      &lt;td&gt;129+&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;https://caniuse.com/mdn-css_at-rules_starting-style&quot;&gt;link&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;closedby&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;134+&lt;/td&gt;
      &lt;td&gt;Not yet&lt;/td&gt;
      &lt;td&gt;141+&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;a href=&quot;https://caniuse.com/mdn-html_elements_dialog_closedby&quot;&gt;link&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Safari support for &lt;code class=&quot;highlighter-rouge&quot;&gt;closedby&lt;/code&gt; is still pending. For production use today, add a polyfill: &lt;a href=&quot;https://github.com/fractaledmind/dialog-closedby-polyfill&quot;&gt;dialog-closedby-polyfill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you need invoker command support for older browsers, there is also a polyfill for that: &lt;a href=&quot;https://github.com/keithamus/invokers-polyfill&quot;&gt;invokers-polyfill&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both polyfills are small and only run when native support is missing.&lt;/p&gt;

&lt;h2 id=&quot;integrating-with-turbos-confirm-system&quot;&gt;Integrating with Turbo’s Confirm System&lt;/h2&gt;

&lt;p&gt;Now, what if you want to keep using Turbo’s &lt;code class=&quot;highlighter-rouge&quot;&gt;data-turbo-confirm&lt;/code&gt; attribute while getting a styled native dialog?&lt;/p&gt;

&lt;p&gt;Turbo provides &lt;a href=&quot;https://turbo.hotwired.dev/reference/drive#turbo.config.forms.confirm&quot;&gt;Turbo.config.forms.confirm&lt;/a&gt; for exactly this. &lt;a href=&quot;https://mhenrixon.com/articles/turbo-confirm&quot;&gt;Mikael Henriksson has an excellent writeup&lt;/a&gt; on this approach and Chris Oliver has a &lt;a href=&quot;https://gorails.com/episodes/custom-hotwire-turbo-confirm-modals&quot;&gt;GoRails video&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;First, add a dialog template to your layout, which you can of course style however you’d like using whatever CSS tooling you have in your app:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;%# app/views/layouts/application.html.erb %&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-confirm-dialog&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;closedby=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;any&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;aria-labelledby=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-confirm-title&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;aria-describedby=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-confirm-message&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;hgroup&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;h3&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-confirm-title&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Confirm&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-confirm-message&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/hgroup&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;commandfor=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-confirm-dialog&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;command=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;close&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      Cancel
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;method=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dialog&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;submit&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;confirm&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        Confirm
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The Confirm button is &lt;code class=&quot;highlighter-rouge&quot;&gt;type=&quot;submit&quot;&lt;/code&gt; inside a &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;form method=&quot;dialog&quot;&amp;gt;&lt;/code&gt;. When submitted, the browser closes the dialog and sets &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/returnValue&quot;&gt;returnValue&lt;/a&gt; to the button’s &lt;code class=&quot;highlighter-rouge&quot;&gt;value&lt;/code&gt; attribute. This is how we detect which button was pressed — no JavaScript event coordination needed.&lt;/p&gt;

&lt;p&gt;Then configure Turbo:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dialog&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;turbo-confirm-dialog&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;messageElement&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;turbo-confirm-message&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;confirmButton&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;button[value=&apos;confirm&apos;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;Turbo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;confirm&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;submitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Fall back to native confirm if dialog isn&apos;t in the DOM&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;confirm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;messageElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;textContent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;message&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Allow custom button text via data-turbo-confirm-button&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buttonText&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;submitter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;turboConfirmButton&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Confirm&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;confirmButton&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;textContent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;buttonText&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;showModal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;returnValue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;confirm&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;once&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The JavaScript does only three things:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;set the message text,&lt;/li&gt;
  &lt;li&gt;customize the button text if provided, and&lt;/li&gt;
  &lt;li&gt;open the dialog.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else — closing on button click, closing on Escape, closing on backdrop click, determining which button was pressed — is handled by the platform.&lt;/p&gt;

&lt;p&gt;The fallback to native &lt;code class=&quot;highlighter-rouge&quot;&gt;confirm()&lt;/code&gt; ensures your app still works if the dialog element is missing (e.g., on a different layout or error page).&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Turbo.config.forms.confirm&lt;/code&gt; expects a function returning a Promise that resolves to &lt;code class=&quot;highlighter-rouge&quot;&gt;true&lt;/code&gt; (proceed) or &lt;code class=&quot;highlighter-rouge&quot;&gt;false&lt;/code&gt; (cancel). The function receives three arguments: the confirmation message, the element with the &lt;code class=&quot;highlighter-rouge&quot;&gt;data-turbo-confirm&lt;/code&gt; attribute, and the submitter element. We listen for the &lt;code class=&quot;highlighter-rouge&quot;&gt;close&lt;/code&gt; event and check &lt;code class=&quot;highlighter-rouge&quot;&gt;returnValue&lt;/code&gt;. Write this handler once, add one dialog to your layout, and every &lt;code class=&quot;highlighter-rouge&quot;&gt;data-turbo-confirm&lt;/code&gt; in your app uses it.&lt;/p&gt;

&lt;p&gt;You can customize the confirm button text per-trigger using &lt;code class=&quot;highlighter-rouge&quot;&gt;data-turbo-confirm-button&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;button_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
              &lt;span class=&quot;ss&quot;&gt;method: :delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
              &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;ss&quot;&gt;turbo_confirm: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Are you sure you want to delete this item?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;ss&quot;&gt;turbo_confirm_button: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Delete item&quot;&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  Delete
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This produces a more contextual confirmation dialog with “Delete item” instead of a generic “Confirm” button — better UX that makes the action clear.&lt;/p&gt;

&lt;h2 id=&quot;addendum-preventing-background-scroll&quot;&gt;Addendum: Preventing Background Scroll&lt;/h2&gt;

&lt;p&gt;One common modal requirement is preventing page scroll while the dialog is open:&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;:has&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;dialog&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;:modal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;overflow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:modal&quot;&gt;:modal&lt;/a&gt; pseudo-class matches dialogs opened with &lt;code class=&quot;highlighter-rouge&quot;&gt;showModal()&lt;/code&gt;. Combined with &lt;code class=&quot;highlighter-rouge&quot;&gt;:has()&lt;/code&gt;, this selector targets the body only when a modal dialog is open. When the dialog opens, scrolling stops. When it closes, scrolling resumes. The browser handles the coordination.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Upgrading the default data-turbo-confirm with a beautiful, native HTML dialog with animations</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/turbo-dialog.png" />
          <media:content medium="image" url="https://boringrails.com/images/turbo-dialog.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Hotwire components that refresh themselves</title>
        <link href="https://boringrails.com/articles/self-updating-components/" rel="alternate" type="text/html" title="Hotwire components that refresh themselves" />
        <published>2025-07-07T13:00:00+00:00</published>
        <updated>2025-07-07T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/self-updating-components</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/self-updating-components/">&lt;p class=&quot;guest-intro&quot;&gt;This is a guest collaboration with &lt;a href=&quot;https://x.com/jespr&quot;&gt;Jesper Christiansen&lt;/a&gt;, a long-time fan of most things Ruby on Rails.&lt;/p&gt;

&lt;p&gt;Earlier this year, I made a &lt;a href=&quot;https://x.com/_swanson/status/1895567431189557290&quot;&gt;quick tweet&lt;/a&gt; about a pattern that I think makes working with Hotwire apps much better.&lt;/p&gt;

&lt;p&gt;When you need to make a bit of UI that runs some code in the background, you can use &lt;code class=&quot;highlighter-rouge&quot;&gt;turbo_streams&lt;/code&gt; to ‘refresh’ the front-end when the work starts, is in-progress, and finally when it’s finished.&lt;/p&gt;

&lt;p&gt;But the problem is that, out of the box, &lt;code class=&quot;highlighter-rouge&quot;&gt;turbo_streams&lt;/code&gt; use partials. And frankly, it just kind of sucks to work with. I found myself annoying by having to match up &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt;s across files and passing in data as locals. And it’s hard to search the codebase for partials, leading to cases where you change a partial and break something elsewhere just because it wasn’t easy to find usage.&lt;/p&gt;

&lt;p&gt;Over time, this has lead me toward a pattern that I hadn’t seen elsewhere:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Make a component for the view (and associated logic/helpers)&lt;/li&gt;
  &lt;li&gt;When you do something on the backend, use &lt;code class=&quot;highlighter-rouge&quot;&gt;turbo_stream.replace&lt;/code&gt; and take advantage of the &lt;code class=&quot;highlighter-rouge&quot;&gt;renderable&lt;/code&gt; option to pass the component object instead of a partial + locals&lt;/li&gt;
  &lt;li&gt;Do the same thing later when broadcasting from a job&lt;/li&gt;
  &lt;li&gt;Let the component encapsulate implementation details like the &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_ids&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionCable&lt;/code&gt; broadcast channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So let’s dive into the details!&lt;/p&gt;

&lt;h2 id=&quot;hotwire-components-should-refresh-themselves&quot;&gt;Hotwire: Components should refresh themselves&lt;/h2&gt;

&lt;p&gt;Let’s say you have a user card component that show some basic information about the user, as well as if we have sent them an introduction email or not.&lt;/p&gt;

&lt;p&gt;We want the card to have a button that will send the user an introduction email when clicked - with in-progress state shown in the component, until we’re done sending.&lt;/p&gt;

&lt;p&gt;We would like to keep the user card updated in the places where it’s shown, and we do that using Hotwire’s refresh functionality.&lt;/p&gt;

&lt;h3 id=&quot;a-tangled-mess-of-identifiers&quot;&gt;A tangled mess of identifiers&lt;/h3&gt;

&lt;p&gt;You’ll typically perform those refreshes with something like this in a background job (or model):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Turbo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;StreamsChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;broadcast_replace_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;my-unique-identifier&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;target: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user/card&quot;&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;locals: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This isn’t bad at all! But.. What is this &lt;code class=&quot;highlighter-rouge&quot;&gt;&quot;my-unique-identifier&quot;&lt;/code&gt; string we refer to? That’s of course the &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; of the element we need replaced.&lt;/p&gt;

&lt;p&gt;So inside our partial we have something like&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;my-unique-identifier&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
 ...
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Of course in practice we’d use the &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; method from &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionView::RecordIdentifier&lt;/code&gt; - so we can do &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_id(@user, :user_card)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We’ll go ahead and change the places that refresh our &lt;code class=&quot;highlighter-rouge&quot;&gt;user_card&lt;/code&gt; partial to use &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionView::RecordIdentifier.dom_id(@user, :user_card)&lt;/code&gt; instead of the previously hardcoded &lt;code class=&quot;highlighter-rouge&quot;&gt;my-unique-identifier-string&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not too bad, but less than ideal! In this simple walkthrough of how to implement this functionality, we’ve now already run into the issue; that when the &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; of the component changes, we need to go through our code-base and update all the places that referenced this magic string. Fingers crossed that our tests are in place and we didn’t miss anything! 😅&lt;/p&gt;

&lt;p&gt;In a bigger app, there might be many places where we need to perform updates like these.&lt;/p&gt;

&lt;p&gt;A way of cleaning this up could be to introduce a new method in a &lt;code class=&quot;highlighter-rouge&quot;&gt;user_helper.rb&lt;/code&gt; or similar, something like:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;user_card_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:user_card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we could use said helper method in all the places we need it. It’s better! But it feels decoupled from the core logic of the &lt;code class=&quot;highlighter-rouge&quot;&gt;user_card&lt;/code&gt; partial.&lt;/p&gt;

&lt;p&gt;If in the future we need to get rid of the partial, we’ll have to remember to clean this up or it’ll be a piece of history that isn’t used anymore. Sigh.&lt;/p&gt;

&lt;p&gt;And we haven’t even talked about &lt;code class=&quot;highlighter-rouge&quot;&gt;turbo_stream_from [@user, :user_card_refresh]&lt;/code&gt; which is another thing that needs to be in sync in places where it’s needed - and it might even be conditional on some state.&lt;/p&gt;

&lt;h3 id=&quot;view-components-to-the-rescue&quot;&gt;View Components to the rescue&lt;/h3&gt;

&lt;p&gt;I find something like &lt;a href=&quot;https://github.com/ViewComponent/view_component&quot;&gt;ViewComponent&lt;/a&gt; (..or &lt;a href=&quot;https://www.phlex.fun/&quot;&gt;Phlex&lt;/a&gt;, or whatever gets you excited) great for use-cases like these. We can build components with a greater level of complexity and have everything packaged neatly into a single Ruby class.&lt;/p&gt;

&lt;p&gt;This means that component isn’t just responsible for what it renders, but it can also become responsible for refreshing it’s own content.&lt;/p&gt;

&lt;p&gt;Are we mixing responsibilities and breaking the sacred Single Responsibility Principle? Probably, but I find that I much prefer working in systems that value &lt;a href=&quot;https://htmx.org/essays/locality-of-behaviour/&quot;&gt;locality of behavior&lt;/a&gt; these days.&lt;/p&gt;

&lt;p&gt;Let’s take a look at our simple &lt;code class=&quot;highlighter-rouge&quot;&gt;UI::UserCard&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;(Side note: I love using &lt;code class=&quot;highlighter-rouge&quot;&gt;UI&lt;/code&gt; module for components because it’s super clear this object is a view component and it lets you drop the “Component” suffix that people tend to use for every…single…component in their apps. Highly recommend!)&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UI::UserCard&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationComponent&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:user_card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;broadcast_channel&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:user_card_refresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;broadcast_refresh!&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Turbo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;StreamsChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;broadcast_replace_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;broadcast_channel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;target: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;renderable: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;layout: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can then use our &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;broadcast_channel&lt;/code&gt; methods in our component’s view:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;helpers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;turbo_stream_from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_channel&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-lg font-bold&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-sm text-slate-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We’re now referencing the &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; from our component, and streaming from our &lt;code class=&quot;highlighter-rouge&quot;&gt;broadcast_channel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This simplifies things quite a bit in places where we’re performing updates which causes us to want to re-render the &lt;code class=&quot;highlighter-rouge&quot;&gt;UI::UserCard&lt;/code&gt; for a given user.&lt;/p&gt;

&lt;p&gt;We can now make use of the &lt;code class=&quot;highlighter-rouge&quot;&gt;broadcast_refresh!&lt;/code&gt; method on the component itself. The following code can be called from a background job, the model or wherever you’re making an update you want to broadcast to other users.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;UI&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;UserCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;broadcast_refresh!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The component knows how to refresh itself and all of the “magic” identifiers we relied on are kept in one central place within the component itself.&lt;/p&gt;

&lt;p&gt;That way we can use it in our controller when we send a new introduction email to a user:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send_introduction_email_later!&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;user_card&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UI&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;UserCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;sending_email: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_stream: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;turbo_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This ends up enqueuing a background job via the &lt;code class=&quot;highlighter-rouge&quot;&gt;send_introduction_email_later!&lt;/code&gt; method on the user and it responds with a turbo_stream to replace the &lt;code class=&quot;highlighter-rouge&quot;&gt;UI::UserCard&lt;/code&gt; for the user that clicked the button that triggered this action.&lt;/p&gt;

&lt;p&gt;Notice how we’re making use of the same &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; method on the user component as we use in the refresh example. So if we ever change this in the future, we need to update it in one place.&lt;/p&gt;

&lt;h3 id=&quot;do-we-need-to-keep-the-stream-open-for-eternity&quot;&gt;Do we need to keep the stream open for eternity?&lt;/h3&gt;

&lt;p&gt;Keeping the stream open for forever might not be ideal - especially since we only really want to update the sending state for a user, and nothing else.&lt;/p&gt;

&lt;p&gt;Let’s introduce a &lt;code class=&quot;highlighter-rouge&quot;&gt;sending_email?&lt;/code&gt; state to our &lt;code class=&quot;highlighter-rouge&quot;&gt;UI::UserCard&lt;/code&gt; to show that we’re sending an email. We can also use this state to only stream from the channel when we’re in the middle of sending an email.&lt;/p&gt;

&lt;p&gt;Let’s update our component’s initialize with a &lt;code class=&quot;highlighter-rouge&quot;&gt;sending_email?&lt;/code&gt; method&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UI::UserCard&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationComponent&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;sending_email: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@sending_email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sending_email&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sending_email?&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@sending_email&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;introduction_email_sent?&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;introduction_email_sent_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present?&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In our component’s view, we’ll add a conditional for this new &lt;code class=&quot;highlighter-rouge&quot;&gt;sending_email&lt;/code&gt; state&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-lg font-bold&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-sm text-slate-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sending_email?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;helpers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;turbo_stream_from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_channel&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;UI&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Spinner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;size: :sm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;message: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Sending introduction email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;elsif&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;introduction_email_sent?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;helpers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;button_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Send introduction email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_emails_introduction_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then in our background job that sends the introduction email:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SendUserIntroductionEmailJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;queue_as&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:default&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;send_introduction_email_later!&lt;/span&gt;

    &lt;span class=&quot;no&quot;&gt;UI&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;UserCard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;sending_email: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;broadcast_refresh!&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We now replace the &lt;code class=&quot;highlighter-rouge&quot;&gt;UI::UserCard&lt;/code&gt; with a version that has &lt;code class=&quot;highlighter-rouge&quot;&gt;sending_email&lt;/code&gt; set to &lt;code class=&quot;highlighter-rouge&quot;&gt;true&lt;/code&gt;, which means we’ll subscribe to updates from the &lt;code class=&quot;highlighter-rouge&quot;&gt;broadcast_channel&lt;/code&gt; and showing an in-progress state in the component. When that work finishes, we replace the component with the &lt;code class=&quot;highlighter-rouge&quot;&gt;sending_email: false&lt;/code&gt; equivalent, which doesn’t listen to updates anymore.&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;Wrapping parts of the UI in a component containing business logic using something like &lt;a href=&quot;https://github.com/ViewComponent/view_component&quot;&gt;ViewComponent&lt;/a&gt; can be a huge benefit.&lt;/p&gt;

&lt;p&gt;It keeps the logic encapsulated, and introduces a clear way to do things like refreshing the UI when something changes.&lt;/p&gt;

&lt;p&gt;It also makes refactoring easier in the future, since we have one place where we have the component’s &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; and one place where we defined the channel to push updates to (&lt;code class=&quot;highlighter-rouge&quot;&gt;broadcast_channel&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Often times, keeping behavior together can make it much easier to work in a codebase than following a strict adherence to separation of concerns.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Using ViewComponents that know how to refresh themselves via turbo_streams is a powerful pattern to build complex flows with Hotwire</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/goat-pattern.png" />
          <media:content medium="image" url="https://boringrails.com/images/goat-pattern.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Event sourcing for smooth brains: building a basic event-driven system in Rails</title>
        <link href="https://boringrails.com/articles/event-sourcing-for-smooth-brains/" rel="alternate" type="text/html" title="Event sourcing for smooth brains: building a basic event-driven system in Rails" />
        <published>2024-07-21T13:00:00+00:00</published>
        <updated>2024-07-21T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/event-sourcing-for-smooth-brains</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/event-sourcing-for-smooth-brains/">&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 id=&quot;why-did-i-go-down-this-path&quot;&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=&quot;https://arrows.to/&quot;&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 id=&quot;its-about-the-events&quot;&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: a basic version of GitHub Issues.&lt;/p&gt;

&lt;p&gt;Create an &lt;code class=&quot;highlighter-rouge&quot;&gt;issue_events&lt;/code&gt; table with this schema (adjust to your liking, but this is the basic structure I use):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;create_table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:issue_events&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;foreign_key: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;foreign_key: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;to_table: :users&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;index: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;polymorphic: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;

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

  &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;datetime&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:occurred_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;CURRENT_TIMESTAMP&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;timestamps&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In a Rails app, I like making the event model under the &lt;code class=&quot;highlighter-rouge&quot;&gt;Issue&lt;/code&gt; namespace, especially when you are using such a common name as “event”.&lt;/p&gt;

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

  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class_name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Issue::Event&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Issue::Event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:issue&lt;/span&gt;

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

&lt;h2 id=&quot;the-event-model&quot;&gt;The event model&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;occurred_at&lt;/code&gt;: the time the event occurred&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;record&lt;/code&gt;: an optional polymorphic association to the record that was acted on (e.g. a &lt;code class=&quot;highlighter-rouge&quot;&gt;Comment&lt;/code&gt; or a &lt;code class=&quot;highlighter-rouge&quot;&gt;Label&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;extra&lt;/code&gt;: a JSONB column for storing any extra data that might be needed. Generally be a bit wary of this because it will be unstructured, but for basic things it’s fine&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Issue::Event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:issue&lt;/span&gt;

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

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

  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;inclusion: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;in: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SUPPORTED_ACTIONS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;message: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;%{value} is not a valid action&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&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;/p&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;record_event!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;actor: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;record: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;extra: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;throttle_within: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;throttle_within&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present?&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;action: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;record: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;actor: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;occurred_at: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;throttle_within&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ago&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;touch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:occurred_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;action: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;record: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;actor: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;extra: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;extra&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Recording events&lt;/span&gt;
&lt;span class=&quot;vi&quot;&gt;@issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;record_event!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:comment_added&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;actor: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;record: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

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

&lt;span class=&quot;c1&quot;&gt;# Adding some extra data for extra bits of metadata&lt;/span&gt;
&lt;span class=&quot;vi&quot;&gt;@issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;record_event!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:label_added&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;extra: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We add a &lt;code class=&quot;highlighter-rouge&quot;&gt;record_event!&lt;/code&gt; method to the &lt;code class=&quot;highlighter-rouge&quot;&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, record, and actor that occurred within the throttle time period and touch it to update the &lt;code class=&quot;highlighter-rouge&quot;&gt;occurred_at&lt;/code&gt; timestamp.&lt;/p&gt;

&lt;h2 id=&quot;voila-an-activity-feed&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Issues::FeedsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@issue&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:issue_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@events&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;occurred_at: :desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;items: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Create a view or component to render each event in the feed&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# For each item you have `action`, `actor`, `occurred_at`, and `record`&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# to construct a line item. You can define icons for each type, different&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# colors, etc (exercise left to the reader)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Issues&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;UI&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Feed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@events&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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=&quot;https://arrows.to/&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;after_create_commit&lt;/code&gt; callbacks to do this.&lt;/p&gt;

&lt;p class=&quot;pro-tip&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Issue::Event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:broadcast&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;broadcast&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Email&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Inbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;process_later&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;AppNotification&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Inbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;process_later&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Slack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Inbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;process_later&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Add whatever makes sense for your app&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Email::Inbox&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;process_later&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;perform_later&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;process&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sym&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:comment_added&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Send an email to the issue creator&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Send an email to any people subscribed to the issue&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Send an email to any project maintainers with notifications enabled&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:label_added&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:comment_viewed&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;process&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&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 id=&quot;reaping-what-youve-sown&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Linear::Inbox&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;#...&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;process&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;synced_to_linear?&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sym&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:comment_added&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Linear&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;API&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_comment!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;#...&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you want to build a basic workflow automation system, you have a great start.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Issue::Workflow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:issue&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:conditions&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:actions&lt;/span&gt;

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

&lt;span class=&quot;vi&quot;&gt;@issue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;workflows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;triggered_on: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;comment_added&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;conditions: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;attribute: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operator: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;value: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2022-01-01&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;actions: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;type: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;reply&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;message: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;This issue is stale, open a new one&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Issue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;comment_deleted&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;issue: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;issues&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;/p&gt;

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

&lt;p&gt;This pattern has been powerful for us at &lt;a href=&quot;https://arrows.to/&quot;&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 id=&quot;acknowledgements-and-further-reading&quot;&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=&quot;https://x.com/DCoulbourne&quot;&gt;Daniel Coulbourne&lt;/a&gt; and &lt;a href=&quot;https://cmorrell.com/&quot;&gt;Chris Morrell&lt;/a&gt;in the context of their Laravel package &lt;a href=&quot;https://verbs.thunk.dev/&quot;&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=&quot;https://martinfowler.com/eaaDev/EventSourcing.html&quot;&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;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Event sourcing is a jargon filled mess, but we can build a lean version with just ActiveRecord, callbacks, and a bit of boring code. Learn how to create simple, yet powerful event-driven systems in Rails.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/smooth-brain.png" />
          <media:content medium="image" url="https://boringrails.com/images/smooth-brain.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Writing better Action Mailers: Revisiting a core Rails concept</title>
        <link href="https://boringrails.com/articles/writing-better-action-mailers/" rel="alternate" type="text/html" title="Writing better Action Mailers: Revisiting a core Rails concept" />
        <published>2023-01-16T13:00:00+00:00</published>
        <updated>2023-01-16T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/writing-better-action-mailers</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/writing-better-action-mailers/">&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;div class=&quot;do-this mb-6&quot;&gt;
  Psst! If you like thinking about software and writing code in the &quot;Boring Rails&quot; style, we are &lt;a href=&quot;https://www.notion.so/arrows/Product-Engineer-Ruby-on-Rails-b59c7c818d4c447095d54e9171a32bc3&quot;&gt;hiring Product Engineers at Arrows&lt;/a&gt;. Come work with me!
&lt;/div&gt;

&lt;h2 id=&quot;better-display-names&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ActionMailer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;help@arrows.to&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Arrows HQ&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Arrows HQ &amp;lt;help@arrows.to&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ActionMailer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;display_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Matt Swanson &amp;lt;matt@boringrails.com&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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=&quot;https://boringrails.com/tips/rails-mailers-email-address-with-name&quot;&gt;written about this helper before&lt;/a&gt; but every time I mention it, someone replies telling me this is the first they’ve heard of it, so I will keep repeating it!&lt;/p&gt;

&lt;h2 id=&quot;changing-the-view-folders&quot;&gt;Changing the view folders&lt;/h2&gt;

&lt;p&gt;One thing that always bugs me is the file structure for mailer views. By default, the view for, e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;NotificationMailer.welcome_email&lt;/code&gt; will be located at &lt;code class=&quot;highlighter-rouge&quot;&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=&quot;https://andycroll.com/ruby/all-your-mailer-views-in-one-place/&quot;&gt;put all your mailer views in one place&lt;/a&gt;. If you make one small tweak to your &lt;code class=&quot;highlighter-rouge&quot;&gt;ApplicationMailer&lt;/code&gt;, you can achieve a more scannable folder structure.&lt;/p&gt;

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

&lt;p&gt;Now you can put your mailer view in &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;app/views/mailers&lt;/code&gt; folder instead of mixing in mailer views with controller views.&lt;/p&gt;

&lt;h2 id=&quot;multiple-emails-per-mailer&quot;&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 class=&quot;highlighter-rouge&quot;&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;/p&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;comment_reply_email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# mail(to: ...)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;UserMentionedMailer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;layout&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;minimal&quot;&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mentioned_email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mentionee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# mail(to: ...)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;/p&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;comment_reply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# mail(to: ...)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mentioned&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mentionee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# mail(to: ...)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;you-dont-need-to-write-the-word-email&quot;&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 class=&quot;highlighter-rouge&quot;&gt;_email&lt;/code&gt; to your methods/views – especially if you follow the above tip to put the views into &lt;code class=&quot;highlighter-rouge&quot;&gt;app/views/mailers&lt;/code&gt;.&lt;/p&gt;

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

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

&lt;h2 id=&quot;parameterized-mailers&quot;&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=&quot;mailto:hello@arrows.to&quot;&gt;hello@arrows.to&lt;/a&gt;, it would come from &lt;a href=&quot;mailto:onboarding@acme-saas.com&quot;&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 class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NotificationMailer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;comment_reply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;from: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;build_from_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;build_from_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_name&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;hello@arrows.to&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;My first thought was that I could add some code to my &lt;code class=&quot;highlighter-rouge&quot;&gt;ApplicationMailer&lt;/code&gt; to check the &lt;code class=&quot;highlighter-rouge&quot;&gt;Current.account&lt;/code&gt;. But I hit a snag because mailers should be sent in a background job (via &lt;code class=&quot;highlighter-rouge&quot;&gt;deliver_later&lt;/code&gt;) and we lose the context of the current request (and thus, the &lt;code class=&quot;highlighter-rouge&quot;&gt;Current&lt;/code&gt; attributes). There are ways around this – like a middleware to pass along the &lt;code class=&quot;highlighter-rouge&quot;&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=&quot;https://guides.rubyonrails.org/action_mailer_basics.html&quot;&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 class=&quot;highlighter-rouge&quot;&gt;params&lt;/code&gt;.&lt;/p&gt;

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

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

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

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;NotificationMailer&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;comment: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;comment_reply&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deliver_later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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=&quot;https://github.com/rails/rails/blob/8a7a9d0a0b27e113e6d0a6087299c4bd41d29738/guides/source/5_1_release_notes.md#parameterized-mailers&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;with&lt;/code&gt; and parameterized mailers is that you can add &lt;code class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NotificationMailer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationMailer&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@from&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;build_from_address&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;comment_reply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;subject: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;New reply&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;from: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mentioned&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mentionee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mentionee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;subject: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;You were mentioned&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;from: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;build_from_address&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_name&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;hello@arrows.to&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;dynamic-defaults&quot;&gt;Dynamic defaults&lt;/h2&gt;

&lt;p&gt;The next piece of the puzzle is making use of mailer &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;default&lt;/code&gt; to set the value dynamically.&lt;/p&gt;

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

  &lt;span class=&quot;c1&quot;&gt;# But...it&apos;s probably more useful to use dynamic values&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;from: &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;build_default_from_address&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;build_default_from_address&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Construct the default from address here&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The nice part about using &lt;code class=&quot;highlighter-rouge&quot;&gt;default&lt;/code&gt; is that individual mailers can override the &lt;code class=&quot;highlighter-rouge&quot;&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 id=&quot;connecting-the-dots&quot;&gt;Connecting the dots&lt;/h2&gt;

&lt;p&gt;By using dynamic defaults and &lt;code class=&quot;highlighter-rouge&quot;&gt;before_action&lt;/code&gt; callbacks, you can have access to the &lt;code class=&quot;highlighter-rouge&quot;&gt;params&lt;/code&gt; hash when configuring the defaults.&lt;/p&gt;

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

  &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;comment_reply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;subject: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;New reply&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mentioned&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mentionee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mentionee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;subject: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;You were mentioned&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;build_default_from_address&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_name&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;hello@arrows.to&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;NotificationMailer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;account: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;comment_reply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deliver_later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 is now clear that this behavior is not specific to just the &lt;code class=&quot;highlighter-rouge&quot;&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;/p&gt;

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

  &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;build_default_from_address&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_sender?&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;custom_email_name&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;hello@arrows.to&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NotificationMailer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AccountMailer&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DigestMailer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AccountMailer&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ParticipationMailer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AccountMailer&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;with(account: @account)&lt;/code&gt; configuration.&lt;/p&gt;

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

  &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@account&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;before_action&lt;/code&gt; will fail with a &lt;code class=&quot;highlighter-rouge&quot;&gt;NoMethodError&lt;/code&gt; if the &lt;code class=&quot;highlighter-rouge&quot;&gt;params&lt;/code&gt; are omitted and by using &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch&lt;/code&gt; it will fail with &lt;code class=&quot;highlighter-rouge&quot;&gt;KeyError: :account&lt;/code&gt; if the caller does not pass in the &lt;code class=&quot;highlighter-rouge&quot;&gt;account&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&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;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 class=&quot;highlighter-rouge&quot;&gt;AccountMailer&lt;/code&gt; base class for emails generated within the scope of an account and &lt;code class=&quot;highlighter-rouge&quot;&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;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Mailers are used in literally every Rails application, but often an after thought where we throw out the rules of software design. Revisiting the tools provided by Action Mailer can help us improve how we write mailers.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/action-mailer.png" />
          <media:content medium="image" url="https://boringrails.com/images/action-mailer.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Sorting ActiveRecord results by enum values (in SQL)</title>
        <link href="https://boringrails.com/tips/rails-active-record-sort-by-enum" rel="alternate" type="text/html" title="Sorting ActiveRecord results by enum values (in SQL)" />
        <published>2022-10-10T13:00:00+00:00</published>
        <updated>2022-10-10T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-sorting-by-enum-values</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-active-record-sort-by-enum">&lt;p&gt;Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;enums&lt;/code&gt; are a great way to model things like a status on an ActiveRecord model. They provide a set of human-readable methods while storing the result as an integer in the database.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JobSubmission&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;draft: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;submitted: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;hold: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;rejected: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;accepted: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;canceled: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It is highly recommended to use a Hash to explicitly define the enum values – otherwise Rails will use the index of the enum value when storing it to the database. If you were were to change the order or remove options, you would break the reference mapping.&lt;/p&gt;

&lt;p&gt;Out of the box, you can sort by the enum value like any other column and it will use the value.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;JobSubmission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# &quot;SELECT \&quot;job_submissions\&quot;.* FROM \&quot;job_submissions\&quot; ORDER BY \&quot;job_submissions\&quot;.\&quot;status\&quot; ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This would return &lt;code class=&quot;highlighter-rouge&quot;&gt;JobSubmission&lt;/code&gt; results in ascending order, so first &lt;code class=&quot;highlighter-rouge&quot;&gt;draft&lt;/code&gt;, then &lt;code class=&quot;highlighter-rouge&quot;&gt;submitted&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;hold&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;But what if you wanted sort the results differently? You might be tempted to do something like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JobSubmission&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;STATUS_SORT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;accepted: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;hold: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;submitted: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;draft: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;rejected: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;canceled: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;status_sort_value&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;STATUS_SORT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;JobSubmission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:status_sort_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But this will do the sorting in Ruby, instead of in the database, which can have bad performance for large result sets.&lt;/p&gt;

&lt;p&gt;Instead you can use the &lt;code class=&quot;highlighter-rouge&quot;&gt;in_order_of&lt;/code&gt; to specify the sort order of the enum values. And the best part? The sorting will happen in SQL (using a &lt;code class=&quot;highlighter-rouge&quot;&gt;CASE&lt;/code&gt; command under-the-hood).&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;To sort the &lt;code class=&quot;highlighter-rouge&quot;&gt;JobSubmission&lt;/code&gt; records by the &lt;code class=&quot;highlighter-rouge&quot;&gt;status&lt;/code&gt; enum value in a different order, you can use:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;JobSubmission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;in_order_of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sx&quot;&gt;%w[accepted hold submitted draft rejected canceled]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The first argument is the column to sort by and the second argument is an ordered array of values.&lt;/p&gt;

&lt;p&gt;The SQL generated for this query will be:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;job_submissions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;job_submissions&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;job_submissions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;status&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;IN&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;CASE&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;job_submissions&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;status&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;WHEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;THEN&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;ELSE&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;END&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also add additional sorting to the relation. For instance, you would probably want to sort records by the status and then maybe alphabetically by the &lt;code class=&quot;highlighter-rouge&quot;&gt;name&lt;/code&gt; column to break ties.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;JobSubmission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;in_order_of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sx&quot;&gt;%w[accepted hold submitted draft rejected canceled]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;extra-notes&quot;&gt;Extra notes&lt;/h2&gt;

&lt;p&gt;You important caveat is that &lt;code class=&quot;highlighter-rouge&quot;&gt;in_order_of&lt;/code&gt; also adds a &lt;code class=&quot;highlighter-rouge&quot;&gt;WHERE/IN&lt;/code&gt; clause for the enum values. So only records with enum values that you specify when calling &lt;code class=&quot;highlighter-rouge&quot;&gt;in_order_of&lt;/code&gt; will be returned. This is somewhat surprising and something to watch out for if you are frequently changing the enum values.&lt;/p&gt;

&lt;p&gt;You will probably want to codify the sort order in the model as a scope to avoid having to repeat the ordering in multiple places.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JobSubmission&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:in_status_order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in_order_of&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sx&quot;&gt;%w[accepted hold submitted draft rejected canceled]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;JobSubmission&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;in_status_order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-in_order_of&quot;&gt;ActiveRecord::QueryMethods#in_order_of&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        
          <category term="product" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Thinking in Hotwire: Progressive Enhancement</title>
        <link href="https://boringrails.com/articles/thinking-in-hotwire-progressive-enhancement/" rel="alternate" type="text/html" title="Thinking in Hotwire: Progressive Enhancement" />
        <published>2022-08-16T13:00:00+00:00</published>
        <updated>2022-08-16T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/thinking-in-hotwire-progressive-enhancement</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/thinking-in-hotwire-progressive-enhancement/">&lt;p&gt;There are many tutorials about how to get started with &lt;a href=&quot;https://hotwired.dev/&quot;&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=&quot;https://turbo.hotwired.dev/reference/drive&quot;&gt;Turbo Drive&lt;/a&gt;, &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames&quot;&gt;Frames&lt;/a&gt;, &lt;a href=&quot;https://turbo.hotwired.dev/reference/streams&quot;&gt;Streams&lt;/a&gt;, &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt;, &lt;a href=&quot;https://turbo.hotwired.dev/handbook/native&quot;&gt;Turbo Native&lt;/a&gt;, &lt;a href=&quot;https://github.com/hotwired/turbo-ios/blob/main/Docs/Advanced.md#native---javascript-integration&quot;&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=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement&quot;&gt;progressive enhancement&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;scaffolds-and-crud-without-hotwire&quot;&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 class=&quot;highlighter-rouge&quot;&gt;comments_controller&lt;/code&gt; with a &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;comments#create&lt;/code&gt; where the comment model would be saved and then we send a redirect back to the &lt;code class=&quot;highlighter-rouge&quot;&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;img src=&quot;/images/progressive/scaffold-crud.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-turbo-drive&quot;&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 use 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 class=&quot;highlighter-rouge&quot;&gt;+ Add&lt;/code&gt;, instead of a full-page reload of &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;&amp;lt;body&amp;gt;&lt;/code&gt; is swapped, even when we do a &lt;code class=&quot;highlighter-rouge&quot;&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;img src=&quot;/images/progressive/turbo-drive.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-turbo-frames&quot;&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 class=&quot;highlighter-rouge&quot;&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 inside a Turbo Frame with an &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; of &lt;code class=&quot;highlighter-rouge&quot;&gt;comment_123&lt;/code&gt;, Hotwire will look for a matching &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;turbo-frame id=&apos;comment_123&apos;&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 class=&quot;highlighter-rouge&quot;&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;img src=&quot;/images/progressive/turbo-frame.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-turbo-streams&quot;&gt;Adding Turbo Streams&lt;/h2&gt;

&lt;p&gt;Turbo Drive replaces the whole &lt;code class=&quot;highlighter-rouge&quot;&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;img src=&quot;/images/progressive/turbo-stream.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-turbo-streams--action-cable&quot;&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=&quot;https://guides.rubyonrails.org/action_cable_overview.html&quot;&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;img src=&quot;/images/progressive/turbo-stream-cable.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-stimulus&quot;&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=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;detail&amp;gt;&lt;/code&gt;&lt;/a&gt; but I digress…)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/progressive/stimulus.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-custom-components--react--client-side-javascript&quot;&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=&quot;https://trix-editor.org/&quot;&gt;Trix rich text editor&lt;/a&gt;: it is a standard &lt;code class=&quot;highlighter-rouge&quot;&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;img src=&quot;/images/progressive/custom-elements.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-turbo-native&quot;&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;img src=&quot;/images/progressive/turbo-native.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-strada&quot;&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 id=&quot;wrap-it-up&quot;&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;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Your mental model for Hotwire should be progressive enhancement: start with the basics and layer on Turbo Frames, Streams, and Stimulus as you build more.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/progressive-enhancement.png" />
          <media:content medium="image" url="https://boringrails.com/images/progressive-enhancement.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Galaxy brain CSS tricks with Hotwire and Rails</title>
        <link href="https://boringrails.com/articles/css-tips-and-tricks-hotwire/" rel="alternate" type="text/html" title="Galaxy brain CSS tricks with Hotwire and Rails" />
        <published>2022-07-26T13:00:00+00:00</published>
        <updated>2022-07-26T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/css-tips-and-tricks-hotwire</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/css-tips-and-tricks-hotwire/">&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 id=&quot;empty-states-and-turbo-streams&quot;&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;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my_list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col divide-y&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;list_item&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;collection: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@list&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
      Whoops! you have no items!
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child&quot;&gt;CSS &lt;code class=&quot;highlighter-rouge&quot;&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=&quot;https://twitter.com/_swanson/status/1550557798513131521&quot;&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;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my_list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col divide-y&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;only:block hidden&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Whoops! you have no items!&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;list_item&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;collection: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@list&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Using Tailwind’s &lt;code class=&quot;highlighter-rouge&quot;&gt;only&lt;/code&gt; modifier we set the empty state to have display &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;last-child&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&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 id=&quot;tailwind-variants-with-data-attributes&quot;&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;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;admin?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;button_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Delete&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :delete&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You could instead use a data attribute on the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;body&amp;gt;&lt;/code&gt; of your page to conditionally show admin-related things.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;data-admin&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;admin?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;plugins&lt;/code&gt; section of the Tailwind config file:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;addVariant&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;addVariant&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;admin&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;body[data-admin] &amp;amp;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With this config change you can now use &lt;code class=&quot;highlighter-rouge&quot;&gt;admin:&lt;/code&gt; with any Tailwind classes.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;admin:block hidden&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;button_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Delete&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :delete&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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=&quot;https://twitter.com/marckohlbrugge&quot;&gt;Marc Kohlbrugge&lt;/a&gt; for sharing the idea on &lt;a href=&quot;https://twitter.com/marckohlbrugge/status/1544113650134384640&quot;&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 class=&quot;highlighter-rouge&quot;&gt;viewer:top-0 editor:top-12&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;dynamic-styles-with-erb&quot;&gt;Dynamic styles with erb&lt;/h2&gt;

&lt;p&gt;Don’t forget that you can use generate &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tags dynamically!&lt;/p&gt;

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

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;span&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;author&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;data-user&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;This is actually an &lt;a href=&quot;https://signalvnoise.com/posts/3112-how-basecamp-next-got-to-be-so-damn-fast-without-using-much-client-side-ui&quot;&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;/p&gt;

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

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

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;extend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;colors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;rgb(var(--color-brand) / &amp;lt;alpha-value&amp;gt;)&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;brand-contrast&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;rgb(var(--color-brand-contrast) / &amp;lt;alpha-value&amp;gt;)&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;brand-tint&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;var(--color-brand-tint)&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;h2 id=&quot;stop-string-interpolating-class-names&quot;&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;/p&gt;

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

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;li&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;bg-gray-50 p-2 text-gray-700&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;line-through&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;completed?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://twitter.com/_swanson/status/1341080006169137152&quot;&gt;Rails 6.1&lt;/a&gt; added a &lt;code class=&quot;highlighter-rouge&quot;&gt;class_names&lt;/code&gt; helper method and the &lt;code class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MyWidget&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ViewComponent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;container_classes&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;flex items-center justify-center space-x-2 rounded-full&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;disabled:pointer-events-none disabled:select-none&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;font-medium tracking-wide&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text-white bg-black hover:bg-neutral-900&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variant&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:primary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text-neutral-600 border hover:bg-neutral-50 hover:text-neutral-900&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variant&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:secondary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text-neutral-600 hover:text-neutral-900 hover:bg-neutral-50&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;variant&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:tertiary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;w-full&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;full_width?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&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;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Techniques for working with CSS in Hotwire and Rails that will make you say &quot;wait..you did that with only CSS?!&quot;</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/galaxy-css.png" />
          <media:content medium="image" url="https://boringrails.com/images/galaxy-css.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Adding keyboard shortcuts and hotkeys to StimulusJS</title>
        <link href="https://boringrails.com/articles/stimulus-hotkeys-keyboard-shortcuts/" rel="alternate" type="text/html" title="Adding keyboard shortcuts and hotkeys to StimulusJS" />
        <published>2022-07-11T13:00:00+00:00</published>
        <updated>2022-07-11T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/stimulus-hotkeys-keyboard-shortcuts</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/stimulus-hotkeys-keyboard-shortcuts/">&lt;div class=&quot;do-this mb-6&quot;&gt;
  Keyboard shortcut support for actions is now built-in to &lt;a href=&quot;https://stimulus.hotwired.dev/reference/actions#keyboardevent-filter&quot;&gt;Stimulus 3.2&lt;/a&gt;!
&lt;/div&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 class=&quot;highlighter-rouge&quot;&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=&quot;https://arrows.to/&quot;&gt;Arrows&lt;/a&gt;, I evaluated the ecosystem and wanted to share my thoughts.&lt;/p&gt;

&lt;h2 id=&quot;stimulus-hotkeys&quot;&gt;stimulus-hotkeys&lt;/h2&gt;

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

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hotkeys&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-hotkeys-bindings-value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;{&quot;ctrl+z, command+z&quot;: &quot;#foo-&amp;gt;editor#undo&quot;}&apos;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;foo&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;editor&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The syntax of the &lt;code class=&quot;highlighter-rouge&quot;&gt;bindings&lt;/code&gt; map is similar to the Stimulus action syntax, with the addition of a preceding &lt;code class=&quot;highlighter-rouge&quot;&gt;selector&lt;/code&gt; to find the Stimulus controller to invoke the action on. Internally, &lt;code class=&quot;highlighter-rouge&quot;&gt;stimulus-hotkeys&lt;/code&gt; uses the more general &lt;a href=&quot;https://wangchujiang.com/hotkeys/&quot;&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 id=&quot;stimulus-useusehotkeys&quot;&gt;stimulus-use/useHotkeys&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;stimulus-use&lt;/code&gt; &lt;a href=&quot;https://github.com/stimulus-use/stimulus-use/&quot;&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 class=&quot;highlighter-rouge&quot;&gt;hooks&lt;/code&gt; system, but for Stimulus controllers.&lt;/p&gt;

&lt;p&gt;Included in this collection is &lt;a href=&quot;https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-hotkeys.md&quot;&gt;useHotkeys&lt;/a&gt;. As with &lt;code class=&quot;highlighter-rouge&quot;&gt;stimulus-hotkeys&lt;/code&gt;, the heavy lifting is done by &lt;a href=&quot;https://wangchujiang.com/hotkeys/&quot;&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;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useHotkeys&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus-use&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;useHotkeys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;cmd+t&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;openPalette&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;editFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 on the page, but should not respond to hotkeys when hidden. While &lt;code class=&quot;highlighter-rouge&quot;&gt;useHotkey&lt;/code&gt; 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 id=&quot;hotkeysjs-directly&quot;&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=&quot;https://wangchujiang.com/hotkeys/&quot;&gt;source&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;hotkeys&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hotkeys-js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;hotkeys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;esc&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;doSomething&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;hotkeys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;unbind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;esc&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&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 id=&quot;githubhotkey&quot;&gt;github/hotkey&lt;/h2&gt;

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

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

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

&lt;p&gt;I found this library via the sourcemaps of &lt;a href=&quot;https://www.hey.com/&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;install&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 id=&quot;wrap-it-up&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;HotKey.js&lt;/code&gt; because it best fit my specific requirements.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">A review of the ecosystem for adding hotkeys to your Stimulus controllers: stimulus-hotkeys, stimulus-use/useHotkeys, HotKey.js, and github/hotkey</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/hotkeys.png" />
          <media:content medium="image" url="https://boringrails.com/images/hotkeys.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">The most underrated Rails helper: dom_id</title>
        <link href="https://boringrails.com/articles/rails-dom-id-the-most-underrated-helper/" rel="alternate" type="text/html" title="The most underrated Rails helper: dom_id" />
        <published>2022-06-28T13:00:00+00:00</published>
        <updated>2022-06-28T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/rails-dom-id-the-most-underrated-helper</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/rails-dom-id-the-most-underrated-helper/">&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; takes two arguments: a record and an optional prefix.&lt;/p&gt;

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

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

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;45&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;       &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; &quot;post_45&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; &quot;new_post&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;45&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:edit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; &quot;edit_post_45&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:custom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; &quot;custom_post&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The reason this helper is so nice is that it &lt;a href=&quot;https://rubyonrails.org/doctrine#convention-over-configuration&quot;&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 class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; when building Hotwire apps.&lt;/p&gt;

&lt;h2 id=&quot;super-clean-tag-builders&quot;&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=&quot;https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag&quot;&gt;nice tag builders&lt;/a&gt; that help you avoid drowning in a sea of brackets, braces, and octothorpes.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:comments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;flex flex-col divide-y&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;comments&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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=&quot;https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss&quot;&gt;Tailwind Intellisense&lt;/a&gt; plugin for VS Code to &lt;a href=&quot;https://twitter.com/_swanson/status/1541419976732692485&quot;&gt;autocomplete when using tag helpers&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Take advantage of &lt;a href=&quot;https://twitter.com/_swanson/status/1341080006169137152&quot;&gt;class_name helper&lt;/a&gt; (from the &lt;code class=&quot;highlighter-rouge&quot;&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=&quot;https://boringrails.com/tips/lightweight-components-with-helpers-stimulus&quot;&gt;lightweight helper&lt;/a&gt; or a library like &lt;a href=&quot;https://viewcomponent.org/&quot;&gt;ViewComponent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;deep-linking-anchor-tags&quot;&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 class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; to scroll the browser directly to an element with the corresponding &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt; (or generate a permalink that users can share).&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;View comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;anchor: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CommentsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;comments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;comment_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;anchor: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;:target&lt;/code&gt; pseudo-class to &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:target&quot;&gt;style the element with an ID matching the URL anchor&lt;/a&gt;. In Tailwind, simply use the &lt;a href=&quot;https://tailwindcss.com/docs/hover-focus-and-other-states#target&quot;&gt;target prefix&lt;/a&gt; (e.g. &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;scroll-margin-top&lt;/code&gt; (&lt;a href=&quot;https://tailwindcss.com/docs/scroll-margin&quot;&gt;Tailwind class&lt;/a&gt;: &lt;code class=&quot;highlighter-rouge&quot;&gt;scroll-mt&lt;/code&gt;) is the answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;with-turbo-frames&quot;&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 class=&quot;highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; uses – you guessed it – &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; under the hood. But you can also pass in your own ids as well.&lt;/p&gt;

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

&lt;p&gt;Since a Turbo Frame needs to be unique per page, &lt;code class=&quot;highlighter-rouge&quot;&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=&quot;https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets&quot;&gt;navigate a frame via other links&lt;/a&gt;, it’s a great convention to follow:&lt;/p&gt;

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

&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Elsewhere... --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Edit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_comment_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_frame: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@comment&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;scoping-turbo-stream-responses&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; keeps things consistent.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/plans/quick_edit/update.turbo_stream.erb --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@plan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;plans/title&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@plan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:notes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;plans/notes&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@plan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:assigned&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;plans/assigned&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

&lt;h2 id=&quot;wrap-it-up&quot;&gt;Wrap it up&lt;/h2&gt;

&lt;p&gt;Who would have thought that a simple helper to generate HTML &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;-comments&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">One of the oldest helpers in Rails is also the most underrated. `dom_id` shines for building apps with Hotwire, allowing you to easily target parts of the page without a bunch of nasty string interpolation.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/dom-id.png" />
          <media:content medium="image" url="https://boringrails.com/images/dom-id.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Self-destructing StimulusJS controllers</title>
        <link href="https://boringrails.com/articles/self-destructing-stimulus-controllers/" rel="alternate" type="text/html" title="Self-destructing StimulusJS controllers" />
        <published>2022-06-13T13:00:00+00:00</published>
        <updated>2022-06-13T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/self-destructing-stimulus-controllers</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/self-destructing-stimulus-controllers/">&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;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application/javascript&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.flash-container&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fadeOut&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.items&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;self-destructing&quot;&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 class=&quot;highlighter-rouge&quot;&gt;this.element.remove()&lt;/code&gt;.&lt;/p&gt;

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

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/scroll_to_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

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

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

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targetElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;locationValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This controller takes in a &lt;code class=&quot;highlighter-rouge&quot;&gt;location&lt;/code&gt; value and then scrolls the page to show that element.&lt;/p&gt;

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

&lt;p&gt;For self-destructing controllers, I like to use the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag since &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template&quot;&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 class=&quot;highlighter-rouge&quot;&gt;div&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This pattern works really well with &lt;a href=&quot;https://turbo.hotwired.dev/handbook/streams&quot;&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 class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:tasks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@task&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:tasks&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;template&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;scroll-to&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-scroll-to-location-value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;turbo:load&lt;/code&gt; events, it just works.&lt;/p&gt;

&lt;h2 id=&quot;what-else-could-you-use-this-for&quot;&gt;What else could you use this for?&lt;/h2&gt;

&lt;h3 id=&quot;highlighter&quot;&gt;Highlighter&lt;/h3&gt;

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

&lt;p&gt;&lt;img src=&quot;/images/highlighter-example.png&quot; alt=&quot;Example of highlighter controller&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;template&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;highlighter&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-highlighter-marker-value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;%= dom_id(task, :list_item) %&amp;gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-highlighter-highlight-class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-blue-600 bg-blue-100&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By using both the Stimulus &lt;code class=&quot;highlighter-rouge&quot;&gt;values&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/highlighter_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

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

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

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;markedElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;markerValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;grab-focus&quot;&gt;Grab focus&lt;/h3&gt;

&lt;p&gt;We use a &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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;img src=&quot;/images/grab-focus-example.gif&quot; alt=&quot;Example of grab-focus controller&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/grab_focus_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

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

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

  &lt;span class=&quot;nx&quot;&gt;grabFocus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hasSelectorValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectorValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)?.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;focus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;analytics-beacons&quot;&gt;Analytics “Beacons”&lt;/h3&gt;

&lt;p&gt;We borrowed this idea from &lt;a href=&quot;https://www.hey.com/&quot;&gt;HEY&lt;/a&gt; and use it for tracking page analytics. We add a &lt;code class=&quot;highlighter-rouge&quot;&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=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API&quot;&gt;Beacon Web API&lt;/a&gt;, but we’re justing sending an PATCH request here for simplicity!)&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/beacon_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@rails/request.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

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

  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;urlValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We wrapped this one up in a Rails view helper for a more clean API.&lt;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;AnalyticsHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;tracking_beacon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;template&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;beacon&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;beacon_url_value: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Inside app/views/layouts/plan.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tracking_beacon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plan_viewings_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@plan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&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;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Add sprinkles of Javascript behavior with Stimulus controllers that run a few lines of code and then remove themselves from the page. Like inlined jQuery snippets but for the modern times!</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/self-destruct.png" />
          <media:content medium="image" url="https://boringrails.com/images/self-destruct.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Tailwind style CSS transitions with StimulusJS</title>
        <link href="https://boringrails.com/articles/tailwind-style-css-transitions-with-stimulusjs/" rel="alternate" type="text/html" title="Tailwind style CSS transitions with StimulusJS" />
        <published>2022-06-01T13:00:00+00:00</published>
        <updated>2022-06-01T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/tailwind-style-css-transitions-with-stimulusjs</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/tailwind-style-css-transitions-with-stimulusjs/">&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;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;modalTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;transition&lt;/code&gt; properties, as &lt;a href=&quot;https://twitter.com/sebdedeyne&quot;&gt;Sebastian De Deyne&lt;/a&gt; lays out in this &lt;a href=&quot;https://sebastiandedeyne.com/javascript-framework-diet/enter-leave-transitions/&quot;&gt;excellent primer on enter and leave transitions&lt;/a&gt;, is that you can’t change an element’s &lt;code class=&quot;highlighter-rouge&quot;&gt;display&lt;/code&gt; property before the transition occurs. We can use basic &lt;code class=&quot;highlighter-rouge&quot;&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=&quot;https://vuejs.org/guide/built-ins/transition.html#css-based-transitions&quot;&gt;Vue&lt;/a&gt; and &lt;a href=&quot;https://alpinejs.dev/directives/transition&quot;&gt;Alpine&lt;/a&gt; communities is to use a series of &lt;code class=&quot;highlighter-rouge&quot;&gt;data&lt;/code&gt; attributes to define your desired CSS classes during the lifecycle of the transition. &lt;a href=&quot;https://tailwindui.com/components/application-ui/overlays/slide-overs&quot;&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;img src=&quot;/images/enter-leave-diagram.png&quot; alt=&quot;The lifecycle stages for CSS transitions in Vue&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The approach of using &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;Transition&lt;/code&gt;) and Alpine (&lt;code class=&quot;highlighter-rouge&quot;&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 id=&quot;options&quot;&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=&quot;https://github.com/robbevp/stimulus-transition&quot;&gt;This library&lt;/a&gt; provides a &lt;code class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;transition&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-transition-enter-active=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;enter-class&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-transition-enter-from=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;enter-from-class&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-transition-enter-to=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;enter-to-class&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-transition-leave-active=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;or-use multiple classes&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-transition-leave-from=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;or-use multiple classes&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-transition-leave-to=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;or-use multiple classes&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- content --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;transition:end-enter&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;stimulus-use&lt;/code&gt; &lt;a href=&quot;https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-transition.md&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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=&quot;https://github.com/mmccall10/el-transition&quot;&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 class=&quot;highlighter-rouge&quot;&gt;enter&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;leave&lt;/code&gt; functions directly and then call them while providing the element to transition.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;leave&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;el-transition&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// in your stimulus controller somewhere&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;modalTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;leave&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;modalTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;my-recommendation-el-transition&quot;&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 class=&quot;highlighter-rouge&quot;&gt;data-&lt;/code&gt; attribute transitions.&lt;/p&gt;

&lt;p&gt;I had the most success with &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;enter&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;leave&lt;/code&gt; functions returned &lt;code class=&quot;highlighter-rouge&quot;&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 id=&quot;building-the-tailwind-ui-slide-over-menu&quot;&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=&quot;https://tailwindui.com/&quot;&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 class=&quot;highlighter-rouge&quot;&gt;slide-over&lt;/code&gt; controller, with targets for the three parts in the Tailwind markup (&lt;code class=&quot;highlighter-rouge&quot;&gt;backdrop&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;panel&lt;/code&gt;, and &lt;code class=&quot;highlighter-rouge&quot;&gt;closeButton&lt;/code&gt;) and then one more for the whole menu (&lt;code class=&quot;highlighter-rouge&quot;&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;/p&gt;

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

  Entering: &quot;ease-in-out duration-500&quot;
    From: &quot;opacity-0&quot;
    To: &quot;opacity-100&quot;
  Leaving: &quot;ease-in-out duration-500&quot;
    From: &quot;opacity-100&quot;
    To: &quot;opacity-0&quot;
--&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;data&lt;/code&gt; attributes to match the specification.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;slide-over&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  ...

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

  ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&gt;&amp;lt;button&amp;gt;&lt;/code&gt; to show the panel when clicked.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;leave&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;el-transition&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

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

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

  &lt;span class=&quot;nx&quot;&gt;hide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;leave&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backdropTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;leave&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;closeButtonTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;leave&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;panelTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;containerTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When the &lt;code class=&quot;highlighter-rouge&quot;&gt;show&lt;/code&gt; action is called (by clicking the button), we remove the &lt;code class=&quot;highlighter-rouge&quot;&gt;hidden&lt;/code&gt; class on the whole container and then run the &lt;code class=&quot;highlighter-rouge&quot;&gt;enter&lt;/code&gt; function from &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;data&lt;/code&gt; attributes.&lt;/p&gt;

&lt;p&gt;When we trigger the &lt;code class=&quot;highlighter-rouge&quot;&gt;hide&lt;/code&gt; action (by clicking the close button), we do everything in reverse. We run the &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt; or flashing of content when the transition is finished and then removed!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/slide-over-menu.gif&quot; alt=&quot;Example video of StimulusJS slide-over menu from TailwindUI&quot; /&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 id=&quot;wrap-it-up&quot;&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;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Build polished UI components with StimulusJS and Enter/leave CSS Transitions using patterns from Vue, Alpine, and Tailwind.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/css-transitions.png" />
          <media:content medium="image" url="https://boringrails.com/images/css-transitions.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Dynamic user content in Rails with Liquid tags</title>
        <link href="https://boringrails.com/tips/rails-liquid-dynamic-user-content" rel="alternate" type="text/html" title="Dynamic user content in Rails with Liquid tags" />
        <published>2022-01-23T13:00:00+00:00</published>
        <updated>2022-01-23T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-liquid-dynamic-user-content</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-liquid-dynamic-user-content">&lt;p&gt;When building features that accept user-generated content, you may need to display dynamic content based on what the user specifies. Imagine you want to users to be able to customize a welcome message sent from your application when they invite someone to their account.&lt;/p&gt;

&lt;p&gt;Rails programmers are deeply familiar with writing content with pieces of dynamic text: we do this all the time when writing view templates. But we don’t want to allow users to write ERB or HAML strings and execute them in our app. It’s both a huge security risk and also not super friendly for users to have to learn a complete programming language to change some text.&lt;/p&gt;

&lt;p&gt;An alternative might be use some “magic strings” where you can swap in values for special strings like &lt;code class=&quot;highlighter-rouge&quot;&gt;$NAME&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;*|EMAIL|*&lt;/code&gt;. These are sometimes called “merge tags”. But implementing this approach usually ends up with a soup of &lt;code class=&quot;highlighter-rouge&quot;&gt;gsub&lt;/code&gt;, regular expressions, and weird edge-cases.&lt;/p&gt;

&lt;p&gt;A great option for building this feature is to use &lt;a href=&quot;https://shopify.github.io/liquid/&quot;&gt;Liquid templates&lt;/a&gt;. Liquid is a very striped down templating language that is most commonly used by Shopify for allowing store owners to customize their ecommerce stores.&lt;/p&gt;

&lt;p&gt;Liquid templates solves all these issues.&lt;/p&gt;

&lt;p&gt;There is security because you control the context used when rendering – this is a fancy way of saying that Liquid templates can only access data that you explicitly pass in. The syntax is minimal and it comes with built-in functions for common operations (default values, capitalization, date formats, etc). And the library parses the input and doesn’t rely on fragile regular expressions – instead of randomly breaking, you can catch invalid syntax and handle it appropriately.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Add the &lt;code class=&quot;highlighter-rouge&quot;&gt;liquid&lt;/code&gt; gem to your project.&lt;/p&gt;

&lt;p&gt;The basic operation is two steps:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Create a template from user-input&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Liquid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Hi {{ customer.name }}!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Render the template with the dynamic data&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;customer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Matt&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}})&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;Hi Matt!&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In practice, there are a few conveniences you’ll want to incorporate into your own Rails view helper.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Liquid requires the dynamic data hash to have string keys, whereas Rails apps often use symbol keys for hashes. You can call the Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;deep_stringify_keys&lt;/code&gt; method on a hash to convert them.&lt;/li&gt;
  &lt;li&gt;Calling &lt;code class=&quot;highlighter-rouge&quot;&gt;render!&lt;/code&gt; instead of &lt;code class=&quot;highlighter-rouge&quot;&gt;render&lt;/code&gt; will raise an exception so that you can fallback to returning the raw user-input.&lt;/li&gt;
  &lt;li&gt;Liquid provides &lt;code class=&quot;highlighter-rouge&quot;&gt;strict_variables&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;strict_filters&lt;/code&gt; options that can turn undefined variables or filters into errors. You likely want these both to be true so that users can figure out syntax errors instead of the content silently being blank.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my project, we added this helper method:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/helpers/liquid_helper.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;LiquidHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;liquid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Liquid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;render!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deep_stringify_keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;strict_variables: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;strict_filters: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Liquid&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Error&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And then in any view (or mailer) in your Rails app, you can call the &lt;code class=&quot;highlighter-rouge&quot;&gt;liquid&lt;/code&gt; helper to display dynamic user-generated content.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;vi&quot;&gt;@campaign&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Campaign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;subject: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Welcome {{ customer.name }}!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;liquid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@campaign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;subject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;customer: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@customer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note: if you are using rich-text via &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionText&lt;/code&gt;, you’ll need to call &lt;code class=&quot;highlighter-rouge&quot;&gt;html_safe&lt;/code&gt; after the Liquid interpolation since the output is raw HTML.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;liquid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@campaign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;customer: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@customer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;html_safe&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This helper gracefully handles error cases by returning the original input.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;liquid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Hi {{ missing_value }}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;Hi \{\{ missing_value }}&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;liquid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Hi {{ foo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;Hi \{\{ foo&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You may also want to add convenience methods to your models if you are using them in the Liquid rendering context (instead of building up the context hash every time).&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Customer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:organization&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;to_liquid&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Expose whatever fields you want to be able to use in liquid templates&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;email: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;company_name: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;organization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;liquid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;New sign up from {{ customer.company_name}}. Say hi to {{ customer.name }} &amp;lt;{{ customer.email }}&amp;gt;!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;context: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;customer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_liquid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; New sign up from Arrows. Say hi to Matt &amp;lt;matt@arrows.to&amp;gt;!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;even-more-advanced-battle-tested-features&quot;&gt;Even more advanced battle-tested features&lt;/h2&gt;

&lt;p&gt;You can register your own filters if you want to provide application-specific functions for users like &lt;code class=&quot;highlighter-rouge&quot;&gt;{{ customer | avatar_url }}&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;{{ task.due_date | next_business_day }}&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;{{ &apos;#7ab55c&apos; | color_to_rgb }}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can assign &lt;code class=&quot;highlighter-rouge&quot;&gt;resource_limits&lt;/code&gt; to avoid extremely slow interpolations.&lt;/p&gt;

&lt;p&gt;These are outside the scope of this tip and you can explore on your own.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;Liquid docs: &lt;a href=&quot;https://github.com/Shopify/liquid&quot;&gt;Shopify/liquid&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Liquid for Programmers: &lt;a href=&quot;https://github.com/Shopify/liquid/wiki/Liquid-for-Programmers&quot;&gt;wiki&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Accessing Rails environment variables from a StimulusJS Controller</title>
        <link href="https://boringrails.com/tips/rails-environment-variables-stimulus-js" rel="alternate" type="text/html" title="Accessing Rails environment variables from a StimulusJS Controller" />
        <published>2022-01-20T13:00:00+00:00</published>
        <updated>2022-01-20T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-environment-stimulus-controller</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-environment-variables-stimulus-js">&lt;p&gt;Environment variables are a great way to configure your Rails apps. You can use the &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.env&lt;/code&gt; variable to conditionally change behavior when you are in development/test or production. And you can add your own application specific variables for things like API tokens or global settings.&lt;/p&gt;

&lt;p&gt;While Stimulus has a &lt;code class=&quot;highlighter-rouge&quot;&gt;values&lt;/code&gt; API to read from HTML data attributes, sometimes you need data that you don’t want to pass in every time you create a controller.&lt;/p&gt;

&lt;p&gt;One alternative is to use &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags when rendering your view. Then, inside your Stimulus Javascript code, you can query the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; DOM element to retrieve the value.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;First, add the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag to your application template inside the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt; section. For this example, let’s say we want to know the &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.env&lt;/code&gt; because we want to change a setting when running the app in test mode (I recently had to do this to change a setting in a 3rd-party library to work with headless system tests).&lt;/p&gt;

&lt;p&gt;You can use the Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;tag&lt;/code&gt; helper to avoid a bunch of string interpolation:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Inside the &lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt; tag of your application layout

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:meta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: :rails_env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;content: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;env&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will render a &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag like:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;rails_env&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;development&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, in your Stimulus controller, query for the tag and read the content.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isTestEnvironment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Do something for testing&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;// Do something for dev/prod&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;isTestEnvironment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;meta[name=rails_env]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also extract this into a helper if you want – here is a &lt;a href=&quot;https://github.com/rails/request.js&quot;&gt;utility function&lt;/a&gt; from the Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;request.js&lt;/code&gt; library which uses this same &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tag pattern to access the Rails CSRF token when sending Javascript requests.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;metaContent&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`meta[name=&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;]`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;content&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Remember that this data is visible in your page source! Unlike environment variables that only exist on the server, putting data into your HTML makes it accessible to end-users. Make sure you are only exposing data that is safe to be public.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;MDN: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta&quot;&gt;metadata element&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Rails validations: database level check constraints</title>
        <link href="https://boringrails.com/tips/rails-check-constraints-database-validations" rel="alternate" type="text/html" title="Rails validations: database level check constraints" />
        <published>2022-01-18T13:00:00+00:00</published>
        <updated>2022-01-18T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-check-constraints-database-validations</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-check-constraints-database-validations">&lt;p&gt;One of the most common Rails tips is to back up your ActiveRecord model validations with database level constraints.&lt;/p&gt;

&lt;p&gt;Because there are times when validations are skipped, it’s best to let your database be the
last line of defense to maintain your data integrity. You can add a &lt;code class=&quot;highlighter-rouge&quot;&gt;validates :name, presence: true&lt;/code&gt; line to your model, but if null values sneak into your database, your app will still throw an exception if you call &lt;code class=&quot;highlighter-rouge&quot;&gt;name.downcase&lt;/code&gt; on &lt;code class=&quot;highlighter-rouge&quot;&gt;nil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The most common examples are pairing &lt;code class=&quot;highlighter-rouge&quot;&gt;presence&lt;/code&gt; validations with non-null columns and &lt;code class=&quot;highlighter-rouge&quot;&gt;unique&lt;/code&gt; validations with a unique database index.&lt;/p&gt;

&lt;p&gt;But did you know you can go a step further using a database feature called “check constraints”.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Check constraints run when you attempt to store data into a column. If the data violates the constraint, an error is raised and Rails will rollback the transaction.&lt;/p&gt;

&lt;p&gt;At &lt;a href=&quot;https://arrows.to&quot;&gt;Arrows&lt;/a&gt;, we help onboard customers by creating custom plans. Plans are based on a Template and we recently added a way to set the deadline to a fixed number of days after the Plan gets created.&lt;/p&gt;

&lt;p&gt;We stored the deadline offset as an &lt;code class=&quot;highlighter-rouge&quot;&gt;integer&lt;/code&gt; in the database. But in the context of our application, this number should never be negative. When we create a Plan from the Template, we want to write code like:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;deadline&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deadline_offset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But since the &lt;code class=&quot;highlighter-rouge&quot;&gt;integer&lt;/code&gt; type in the database can be negative, we didn’t have a guarantee that the offset would be positive. Obviously, we don’t want the deadline to ever be before the plan is even created.&lt;/p&gt;

&lt;p&gt;We can validate this at the model level, but we would also like the database to enforce this check.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Template&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:deadline_offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;numericality: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;only_integer: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;greater_than_or_equal_to: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As of Rails 6.1, you can specific check constraints straight from a migration.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AddDeadlineOffsetCheckToTemplates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;7.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;change&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add_check_constraint&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:templates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;deadline_offset &amp;gt;= 0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;deadline_offset_non_negative&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You pass in a &lt;code class=&quot;highlighter-rouge&quot;&gt;name&lt;/code&gt; for the constraint and then the SQL condition to run for the check.&lt;/p&gt;

&lt;p&gt;Now if you forgot the ActiveRecord model validation or accidentally bypass it by skipping callbacks, this check is enforced at the database level.&lt;/p&gt;

&lt;div class=&quot;language-irb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Live dangerously and skip validations!
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deadline_offset&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;validate: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;PG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;CheckViolation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ERROR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relation&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;templates&quot;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;violates&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;check&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;constraint&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;deadline_offset_non_negative&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;StatementInvalid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;other-common-examples&quot;&gt;Other common examples&lt;/h2&gt;

&lt;p&gt;Here are a few more places you might want to use a check constraint:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ensuring a minimum price as a safety mechanism: check that &lt;code class=&quot;highlighter-rouge&quot;&gt;price &amp;gt; 100&lt;/code&gt; to ensure no one accidentally adds a product below a minimum level&lt;/li&gt;
  &lt;li&gt;Validating fixed formats: storing a US zipcode? Make sure &lt;code class=&quot;highlighter-rouge&quot;&gt;char_length(zipcode) = 5&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Enforcing a relationship between two columns: add a constraint that &lt;code class=&quot;highlighter-rouge&quot;&gt;start_date &amp;lt; end_date&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;sale_price &amp;lt;= price&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Should you always add check constraints? I’ve never run into cases where I was unhappy having extra protection when it comes to data integrity. If you have mission-critical data requirements, I would highly recommend adding check constraints.&lt;/p&gt;

&lt;p&gt;Are they strictly necessary for every little thing? Probably not. But you can weigh the headache of fixing data issues with the cost of adding constraints for each validation in your app.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_check_constraint&quot;&gt;add_check_constraint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thoughtbot blog: &lt;a href=&quot;https://thoughtbot.com/blog/validation-database-constraint-or-both&quot;&gt;Validation, Database Constraint, or Both?&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Debugging slow Heroku builds</title>
        <link href="https://boringrails.com/tips/debugging-slow-heroku-builds" rel="alternate" type="text/html" title="Debugging slow Heroku builds" />
        <published>2021-05-17T13:00:00+00:00</published>
        <updated>2021-05-17T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/debugging-slow-heroku-builds</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/debugging-slow-heroku-builds">&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=&quot;https://twitter.com/panozzaj&quot;&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 id=&quot;usage&quot;&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 class=&quot;highlighter-rouge&quot;&gt;heroku-buildpack-timestamps&lt;/code&gt; buildpack!&lt;/p&gt;

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

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&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 &apos;true&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&gt;--verbose&lt;/code&gt; flag for &lt;code class=&quot;highlighter-rouge&quot;&gt;yarn&lt;/code&gt; to see why things were so slow. The official &lt;code class=&quot;highlighter-rouge&quot;&gt;nodejs&lt;/code&gt; buildpack is supposed to automatically cache &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&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 &quot;https://github.com/sass/node-sass/releases/download/v4.13.1/linux-x64-83_binding.node&quot;:

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;/div&gt;

&lt;p&gt;Buried deep in the logs was the culprit. We were trying to download a pre-compiled &lt;code class=&quot;highlighter-rouge&quot;&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 class=&quot;highlighter-rouge&quot;&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;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="heroku" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Quickly explore your data with `uniq` and `tally`</title>
        <link href="https://boringrails.com/tips/explore-data-ruby-uniq-tally-count" rel="alternate" type="text/html" title="Quickly explore your data with `uniq` and `tally`" />
        <published>2021-05-05T13:00:00+00:00</published>
        <updated>2021-05-05T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/quickly-explore-data-with-uniq-tally</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/explore-data-ruby-uniq-tally-count">&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 class=&quot;highlighter-rouge&quot;&gt;uniq&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;tally&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;uniq&lt;/code&gt; method operates on an enumerable and compresses your data down to unique values.&lt;/p&gt;

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

&lt;p&gt;While most developers are familiar with &lt;code class=&quot;highlighter-rouge&quot;&gt;uniq&lt;/code&gt;, the &lt;code class=&quot;highlighter-rouge&quot;&gt;tally&lt;/code&gt; method is one of the best kept secrets in Ruby. The &lt;code class=&quot;highlighter-rouge&quot;&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;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Outreach&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tally&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Confirmed w/o Outreach&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;106&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s2&quot;&gt;&quot;Awaiting Outreach&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;28&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s2&quot;&gt;&quot;Responded&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;48&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s2&quot;&gt;&quot;No Response Expected&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s2&quot;&gt;&quot;Follow-up&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
 &lt;span class=&quot;s2&quot;&gt;&quot;Awaiting Reply&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

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

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

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Improving your Rails mailers with `email_address_with_name`</title>
        <link href="https://boringrails.com/tips/rails-mailers-email-address-with-name" rel="alternate" type="text/html" title="Improving your Rails mailers with `email_address_with_name`" />
        <published>2021-05-03T13:00:00+00:00</published>
        <updated>2021-05-03T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-mailers-email-with-name</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-mailers-email-address-with-name">&lt;p&gt;In almost all email programs, you can add a display name before your email address like so:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;To: Matt Swanson &amp;lt;matt@example.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&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 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Use &lt;code class=&quot;highlighter-rouge&quot;&gt;email_address_with_name&lt;/code&gt; to add a name in-front on an email address in a standard way&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ActionMailer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;swan3788@gmail.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Matt Swanson&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Matt Swanson &amp;lt;swan3788@gmail.com&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;welcome_email&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;mail&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;display_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;subject: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;You have a new message&apos;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;

&lt;p&gt;This helper handles &lt;code class=&quot;highlighter-rouge&quot;&gt;nil&lt;/code&gt; gracefully as well.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ActionMailer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;swan3788@gmail.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;swan3788@gmail.com&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ActionMailer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mike@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Michael J. Scott&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Michael J. Scott&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &amp;lt;mike@example.com&amp;gt;&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;ActionMailer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;email_address_with_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;chip@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;John &quot;Chip&quot; Smith&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;John &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Chip&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; Smith&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &amp;lt;chip@example.com&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

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

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Building lightweight components with Rails Helpers and Stimulus</title>
        <link href="https://boringrails.com/tips/lightweight-components-with-helpers-stimulus" rel="alternate" type="text/html" title="Building lightweight components with Rails Helpers and Stimulus" />
        <published>2021-04-12T13:00:00+00:00</published>
        <updated>2021-04-12T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/lightweight-components-with-helpers-stimulus</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/lightweight-components-with-helpers-stimulus">&lt;p&gt;Custom Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;helpers&lt;/code&gt; modules are often overlooked, but they can be a great option for building lightweight components and reducing boilerplate in your Stimulus controllers.&lt;/p&gt;

&lt;p&gt;One nice thing about Stimulus is that you can quickly infer the functionality just from reading the markup attributes, but for components that have a couple of values and actions, you can benefit from hiding some of the implementation details.&lt;/p&gt;

&lt;p&gt;Let’s take the example from my &lt;a href=&quot;/articles/hovercards-stimulus/&quot;&gt;GitHub-style Hovercards article&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;inline-block&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hovercard&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-hovercard-url-value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hovercard_shoe_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mouseenter-&amp;gt;hovercard#show mouseleave-&amp;gt;hovercard#hide&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;branded-link&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;hovercard_controller&lt;/code&gt; needs to be passed in a &lt;code class=&quot;highlighter-rouge&quot;&gt;url&lt;/code&gt; value and have two actions added for showing and hiding the card on hover. The controller is wrapped around a link that can be styled and customized for each type of hovercard we want in the app.&lt;/p&gt;

&lt;p&gt;If you have only a few places using the controller, it’s not a big deal. But if you want to &lt;a href=&quot;/articles/better-stimulus-controllers/&quot;&gt;re-use this controller&lt;/a&gt; for more and more types of hovercards, try adding your own Rails helper.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Modules in the &lt;code class=&quot;highlighter-rouge&quot;&gt;app/helpers&lt;/code&gt; folder will automatically be available for you to use in your views.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/helpers/hovercard_helper.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;HovercardHelper&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Use a helper to avoid repeating Stimulus controller attributes&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hovercard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;content_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;data-controller&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hovercard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;data-hovercard-url-value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;data-action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mouseenter-&amp;gt;hovercard#show mouseleave-&amp;gt;hovercard#hide&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Build your own light-weight &quot;components&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repo_hovercard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;hovercard&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hovercard_repository_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;user_hovercard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;hovercard&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hovercard_user_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Using a helper allows us to create our own “components” in Ruby to abstract away the implementation details. And because of the power of &lt;a href=&quot;https://www.codewithjason.com/understanding-ruby-blocks/&quot;&gt;Ruby blocks&lt;/a&gt;, we can create flexible components that can be customized per usage.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/timeline.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_hovercard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;repo_hovercard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex items-center space-x-2&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;svg&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Some icon --&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;repository&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For example, we could build a &lt;code class=&quot;highlighter-rouge&quot;&gt;repo_hovercard&lt;/code&gt; helper that accepts a &lt;code class=&quot;highlighter-rouge&quot;&gt;Repository&lt;/code&gt; model and a block to render. We have full control over what to display based on the page context but we don’t have to worry about wiring up Stimulus events correctly.&lt;/p&gt;

&lt;p&gt;And if we want to change our Stimulus controller, it’s all in one spot, instead of spread out across many views in the app.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActionController/Helpers.html&quot;&gt;Helpers&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        
          <category term="stimulusjs" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Combine `redirect_to` and the `anchor` option</title>
        <link href="https://boringrails.com/tips/redirect-to-anchor" rel="alternate" type="text/html" title="Combine `redirect_to` and the `anchor` option" />
        <published>2021-04-01T13:00:00+00:00</published>
        <updated>2021-04-01T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/redirect-to-anchor</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/redirect-to-anchor">&lt;p&gt;Often you’ll have an application screen like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/company-directory.png&quot; alt=&quot;Example company directory&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After editing information about an employee, you’ll redirect back to the Company Directory page.&lt;/p&gt;

&lt;p&gt;For a bit of extra polish, you can redirect with an &lt;code class=&quot;highlighter-rouge&quot;&gt;anchor&lt;/code&gt; to automatically scroll the browser to the recently updated item and maintain your position in the list.&lt;/p&gt;

&lt;p&gt;You can combine this with the &lt;a href=&quot;https://twitter.com/_swanson/status/1341069170503499783&quot;&gt;most underrated Rails helper&lt;/a&gt; – &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; – for a really clean solution.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Your controller will be exactly the same, just tack on the &lt;code class=&quot;highlighter-rouge&quot;&gt;anchor&lt;/code&gt; option to the path helper when redirecting.&lt;/p&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;anchor: dom_id(@employee)&quot; /&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EmployeesController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionView&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RecordIdentifier&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# adds `dom_id`&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@company&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:company_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@employee&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;employees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;employee_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;companies_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;anchor: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;notice: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Updated &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;!&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:edit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: :unprocessable_entity&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;employee_params&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;permit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:department&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Just make sure you use &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; in your view as well:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col divide-y&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;employees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col space-y-2&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;font-bold&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-gray-400&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;department&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;ni&quot;&gt;&amp;amp;middot;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s a small tweak, but it’s easy to do and makes your application just a bit more pleasant.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for&quot;&gt;#url_for Options&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActionView/RecordIdentifier.html#method-i-dom_id&quot;&gt;ActionView::RecordIdentifier#dom_id&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag&quot;&gt;ActionView::TagHelper#content_tag&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Lazy-loading content with Turbo Frames and skeleton loader</title>
        <link href="https://boringrails.com/tips/turboframe-lazy-load-skeleton" rel="alternate" type="text/html" title="Lazy-loading content with Turbo Frames and skeleton loader" />
        <published>2021-03-30T13:00:00+00:00</published>
        <updated>2021-03-30T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/turboframe-lazy-load-skeleton</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/turboframe-lazy-load-skeleton">&lt;p&gt;&lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire&lt;/a&gt; is a new suite of frontend tools from Basecamp for building “reactive Rails” apps while writing a minimal amount of JavaScript.&lt;/p&gt;

&lt;p&gt;While the most exciting feature to some is the real-time streaming of server rendered HTML, my favorite addition is the &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames&quot;&gt;Turbo Frame&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Turbo Frame is a super-charged iFrame that doesn’t make you cringe when you use it. Frames represent a slice of your page and have their own navigation context.&lt;/p&gt;

&lt;p&gt;One incredibly powerful feature is lazy-loading a Frame. One example of this pattern that you probably see everyday is the GitHub activity feed:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/github-feed.gif&quot; alt=&quot;GitHub Activity Feed: Lazy load&quot; /&gt;&lt;/p&gt;

&lt;p&gt;First you load the “outer shell” of the page and then you can make an AJAX call to fetch more content to fill in the rest of the page. It’s a great way to speed up a slow page.&lt;/p&gt;

&lt;p&gt;But one downside is that the page content jumps around a bit. The “Loading” spinner is one small rectangle, but the result is a long feed of events.&lt;/p&gt;

&lt;p&gt;A way to solve this problem is to use a &lt;a href=&quot;https://uxdesign.cc/what-you-should-know-about-skeleton-screens-a820c45a571a&quot;&gt;“skeleton screen” or “skeleton loader”&lt;/a&gt;. This UI pattern uses a blank version of the content as a placeholder and reduces the jarring impact when the content finally loads.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/skeleton-loader.png&quot; alt=&quot;Skeleton loader&quot; /&gt;&lt;/p&gt;

&lt;p&gt;These two concepts go together like peanut butter and jelly.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;A basic lazy-loaded Turbo Frame looks like this:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;feed&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/feed&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  Content will be replaced when /feed has been loaded.
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By specifying the &lt;code class=&quot;highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute, the Frame will automatically make an AJAX request when the page loads and replace its content with the matching &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; in the response.&lt;/p&gt;

&lt;p&gt;Additionally, you can set the &lt;code class=&quot;highlighter-rouge&quot;&gt;loading&lt;/code&gt; property of the loading to be either “eager” (load right away) or “lazy” (load once the frame is visible on the page).&lt;/p&gt;

&lt;p&gt;Here’s how the GitHub Activity feed might look in a Rails view:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/home.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Some other content...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:feed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;src: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;activity_feed_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;loading: :lazy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  Loading...
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can take it to the next level by replacing the basic “Loading…” message with your own skeleton loader. Tailwind makes this really easy with the built-in &lt;code class=&quot;highlighter-rouge&quot;&gt;animate-pulse&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;Simply add some gray rectangles as your initial frame contents:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/home.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;Some other content...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:feed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;src: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;activity_feed_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;loading: :lazy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col space-y-6&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;animate-pulse flex space-x-4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Avatar --&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;rounded-full bg-gray-400 h-12 w-12&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Details --&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex-1 space-y-4 py-1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-4 bg-gray-400 rounded w-3/4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;space-y-2&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-4 bg-gray-400 rounded&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-4 bg-gray-400 rounded w-5/6&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One last thing: make sure your &lt;code class=&quot;highlighter-rouge&quot;&gt;activity_feed_path&lt;/code&gt; action returns the content wrapped in a matching Turbo Frame so that it will automatically swap out the frame contents and replace the loading state.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ActivityFeedController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationControler&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@events&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;activity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note: we don’t want the &lt;code class=&quot;highlighter-rouge&quot;&gt;src&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;loading&lt;/code&gt; attributes set on the Frame in this response, otherwise you would create an infinite loop!&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/activity_feed/show.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:feed&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;feed_item&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;collection: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@events&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once you wrap your head around the power of Turbo Frames, soon you’ll be spotting all kinds of places in your app that can benefit from lazy-loading some good old HTML.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Hotwire Docs: &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames&quot;&gt;Turbo Frames&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tailwind Docs: &lt;a href=&quot;https://tailwindcss.com/docs/animation#pulse&quot;&gt;animate-pulse&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        
          <category term="tailwind" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Building a Rails CI pipeline with GitHub Actions</title>
        <link href="https://boringrails.com/articles/building-a-rails-ci-pipeline-with-github-actions/" rel="alternate" type="text/html" title="Building a Rails CI pipeline with GitHub Actions" />
        <published>2021-03-28T13:00:00+00:00</published>
        <updated>2021-03-28T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/building-a-rails-ci-pipeline-with-github-actions</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/building-a-rails-ci-pipeline-with-github-actions/">&lt;p&gt;&lt;a href=&quot;https://github.com/features/actions&quot;&gt;GitHub Actions&lt;/a&gt; is an automation platform that you run directly from inside a GitHub repository.&lt;/p&gt;

&lt;p&gt;Using GitHub Actions, you build workflows that are triggered by any kind of event. These workflows run arbitrary code as &lt;code class=&quot;highlighter-rouge&quot;&gt;Jobs&lt;/code&gt; and you can piece together multiple &lt;code class=&quot;highlighter-rouge&quot;&gt;Steps&lt;/code&gt; to achieve pretty much whatever you want.&lt;/p&gt;

&lt;p&gt;Aside from automatically posting GIFs on every pull request, the most obvious use-case for this new platform is to build a testing CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/github-actions-example.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;why-bother-im-already-using-circleci--travis--semaphore&quot;&gt;Why bother? I’m already using CircleCI / Travis / Semaphore…&lt;/h2&gt;

&lt;p&gt;Good question. All of these CI/CD platforms are, more or less, equivalent.&lt;/p&gt;

&lt;p&gt;Dedicated CI services like CircleCI are battle-tested for the use-case of running linters, executing tests, reporting results, and kicking off deploys. GitHub Actions is more akin to LEGO – it is a generic platform for running arbitrary workflows and automation.&lt;/p&gt;

&lt;p&gt;On the other hand, GitHub Actions come included in your GitHub projects. For public repositories, GitHub Actions are free (without limitations as of this writing). For private repositories, you get 2000 free build minutes per month per repository – and if you have an existing GitHub company plan, you’re getting upwards of 10k build minutes included at no extra cost. (Full billing/usage limits &lt;a href=&quot;https://help.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions&quot;&gt;here&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;All of the competing platforms have free tiers and, really, the biggest benefit to a project is to have ANY kind of CI/CD tooling set up. The differences between vendors are pretty minor.&lt;/p&gt;

&lt;p&gt;I’m trying to keep my Rails applications as &lt;a href=&quot;/&quot;&gt;boring as possible&lt;/a&gt;. There is enough complexity in building products and solving customer problems that I don’t need to create more work for myself and my team with an exotic ops setup.&lt;/p&gt;

&lt;p&gt;I also work at a custom software consultancy: I help other companies ship products and tools so it is appealing to have one less external service to juggle.&lt;/p&gt;

&lt;p&gt;Setting up CircleCI isn’t hard, but it’s one more account to register, one more place to put a corporate credit card, one more thing that my customer might ask &lt;strong&gt;“hey, what is that for again?”&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Additionally, I like that we can keep our CI pipeline close to the code. One of the biggest benefits in the current wave of CI/CD tooling is the shift to Infrastructure-as-Code: you configure your build using a flat file and commit that with your project. This is miles better than fumbling around a poorly maintained Jenkins instance and changes to your pipeline can be reviewed and merged like the rest of the codebase.&lt;/p&gt;

&lt;p&gt;GitHub Actions pushes this to the extreme. Now you have one single spot for a project’s source code, issue tracker, project management (GitHub Projects), code reviews, security alerts, and now: CI/CD testing. One account, one service, one place to look.&lt;/p&gt;

&lt;h2 id=&quot;migrating-from-circleci-to-github-actions-for-a-common-rails-setup&quot;&gt;Migrating from CircleCI to GitHub Actions for a common Rails setup&lt;/h2&gt;

&lt;p&gt;If you’re like most people, you probably set up your CircleCI configuration on the first week of the project based on a &lt;a href=&quot;https://thoughtbot.com/blog/circleci-2-rails&quot;&gt;thoughtbot blog post&lt;/a&gt; and haven’t touched it since. Me too.&lt;/p&gt;

&lt;p&gt;But no worries, it is straight-forward to move from the CircleCI 2.0 configuration format to the GitHub Actions syntax.&lt;/p&gt;

&lt;p&gt;Your configuration might be a little bit different, but conceptually a vanilla CI/CD pipeline for a Rails app:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Checks out the latest version of the code&lt;/li&gt;
  &lt;li&gt;Sets up a base image with Ruby, Node, and some browser testing stuff&lt;/li&gt;
  &lt;li&gt;Sets up a PostgreSQL database service&lt;/li&gt;
  &lt;li&gt;Installs dependencies (&lt;code class=&quot;highlighter-rouge&quot;&gt;bundler&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;yarn&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;npm&lt;/code&gt;) and cache the results to speed up builds when they don’t change&lt;/li&gt;
  &lt;li&gt;Sets up the test database&lt;/li&gt;
  &lt;li&gt;Runs any linters/checkers (&lt;code class=&quot;highlighter-rouge&quot;&gt;rubocop&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;eslint&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;brakeman&lt;/code&gt;, etc)&lt;/li&gt;
  &lt;li&gt;Runs the tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those steps run in order and if any of them fail, the build fails. You get a red :x: on your commit in GitHub, you can’t merge your PR, someone yells at you in Slack, you know the drill.&lt;/p&gt;

&lt;p&gt;To get a build running on GitHub Actions, we simply translate those high-level actions into the &lt;a href=&quot;https://help.github.com/en/articles/workflow-syntax-for-github-actions&quot;&gt;matching workflow syntax&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One change I found useful to make was to take advantage of GitHub Actions allowing for parallel &lt;a href=&quot;https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobs&quot;&gt;Jobs&lt;/a&gt; to run as part of a single workflow. This feature allows us to run linters and the tests as separate jobs that both must pass for the overall build to pass.&lt;/p&gt;

&lt;p&gt;Most of the competitors do not offer much parallelization on the free/cheap tiers. While each job has to setup the basic environment, we can save a few minutes by running the final steps at the same time. (Note: this approach will use less minutes of wall clock time, but you will use more total build minutes).&lt;/p&gt;

&lt;p&gt;Here is my current GitHub Actions workflow configuration:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Put this in the file: .github/workflows/verify.yml&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Verify&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;linters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Linters&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Checkout code&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Setup Ruby and install gems&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;bundler-cache&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Setup Node&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/setup-node@v2&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;16&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;yarn&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Install packages&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;yarn install --pure-lockfile&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Run linters&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;bin/rubocop --parallel&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;bin/stylelint&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;bin/prettier&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;bin/eslint&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Run security checks&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;bin/bundler-audit --update&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;bin/brakeman -q -w2&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;tests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Tests&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;postgres&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres:12.7&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;POSTGRES_USER&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;myapp&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;POSTGRES_DB&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;myapp_test&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;5432:5432&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Checkout code&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Setup Ruby and install gems&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ruby/setup-ruby@v1&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;bundler-cache&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Setup Node&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/setup-node@v2&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;node-version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;16&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;yarn&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Install packages&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;yarn install --pure-lockfile&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Setup test database&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;RAILS_ENV&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;PGHOST&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;localhost&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;PGUSER&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;myapp&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;bin/rails db:setup&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Run tests&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bin/rspec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Since GitHub Actions is general purpose automation platform (and not solely for CI/CD), you need to tell the action when to run: in this case, we want it to run on every every &lt;code class=&quot;highlighter-rouge&quot;&gt;push&lt;/code&gt;. You can &lt;a href=&quot;https://help.github.com/en/articles/workflow-syntax-for-github-actions#on&quot;&gt;configure this further&lt;/a&gt; to run on only certain branches or files if you wish.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Instead of using a platform-provided Docker image with a bunch of common environment tooling setup (e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;circleci/ruby:2.6.3-node-browsers&lt;/code&gt;), in GitHub Actions you are advised to compose other &lt;a href=&quot;https://github.com/actions&quot;&gt;first-party “setup” actions&lt;/a&gt; that will link in binaries. In this case, we use &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby/setup-ruby&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;actions/setup-node&lt;/code&gt; to include the specific versions of &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;node&lt;/code&gt; that we want to use. These steps are very quick as they are essentially linking into pre-built language binaries.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;You may notice that I am using &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby/setup-ruby&lt;/code&gt; (a &lt;a href=&quot;https://github.com/ruby/setup-ruby&quot;&gt;community action&lt;/a&gt;) instead of the first-party GitHub &lt;code class=&quot;highlighter-rouge&quot;&gt;actions/setup-ruby&lt;/code&gt;. The official action has lagged behind in both features and release versions when it comes to Ruby. During the beta of GitHub Actions, we had to completely stop using Actions because you could not easily pick the specific minor-release version of Ruby and the official action was, in some cases, months behind on supporting the latest Ruby versions (include security releases). The &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby/setup-ruby&lt;/code&gt; action works great – you can pick any version and any flavor (&lt;code class=&quot;highlighter-rouge&quot;&gt;jruby&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;truffleruby&lt;/code&gt;, etc) and it will pull in a pre-built binary in under 5 seconds. Additionally, the &lt;code class=&quot;highlighter-rouge&quot;&gt;ruby/setup-ruby&lt;/code&gt; action supports automatically running &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; and caching your gems without you having to manually fiddle with the &lt;code class=&quot;highlighter-rouge&quot;&gt;cache&lt;/code&gt; action.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Many articles for setting up a PostgreSQL service (including the &lt;a href=&quot;https://github.com/actions/example-services/blob/master/.github/workflows/postgres-service.yml&quot;&gt;official example&lt;/a&gt;) include extra health-check options to make sure the database is started up before proceeding. In my experience, waiting for the health check was taking an extra 15-60 seconds. Since we have to install gems and do other setup before we try to connect to the database, I removed them to shave down the run time and I have not had any problems. :man_shrugging: At the very least, consider changing the health interval settings from the example to something like: &lt;code class=&quot;highlighter-rouge&quot;&gt;--health-interval 10ms --health-timeout 500ms --health-retries 15&lt;/code&gt;. This reduced my “Initializing containers” step from &lt;a href=&quot;https://twitter.com/_swanson/status/1228513469399474176&quot;&gt;~30 seconds to ~10 seconds&lt;/a&gt; on EVERY BUILD.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Experiment with the &lt;code class=&quot;highlighter-rouge&quot;&gt;alpine&lt;/code&gt; Postgres Docker images. These images use a stripped down Linux system that is smaller than the base Docker image. In my testing, the &lt;code class=&quot;highlighter-rouge&quot;&gt;alpine&lt;/code&gt; images were about 33% faster to download and spin up. The trade-off is that you may not be running the exact same environment as your production database, but for projects that aren’t doing anything fancy with Postgres, it seems worth the trade off to me. To use these images, replace e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres:11&lt;/code&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;postgres:11-alpine&lt;/code&gt; in your workflow. You can find a full list of all the &lt;a href=&quot;https://hub.docker.com/_/postgres?tab=tags&quot;&gt;official Postgres images on Dockerhub&lt;/a&gt;. I’ve been running with the &lt;code class=&quot;highlighter-rouge&quot;&gt;alpine&lt;/code&gt; images for ~18 months now and haven’t had any weird issues.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;You should check the various tools you use for options to speed up the run-time in a CI environment. For example, &lt;code class=&quot;highlighter-rouge&quot;&gt;yarn&lt;/code&gt; has a &lt;code class=&quot;highlighter-rouge&quot;&gt;--pure-lockfile&lt;/code&gt; option that tells it not to try to create a lockfile (since you already checked one in…) and &lt;code class=&quot;highlighter-rouge&quot;&gt;rubocop&lt;/code&gt; has a &lt;code class=&quot;highlighter-rouge&quot;&gt;--parallel&lt;/code&gt; flag to save a bit of time. When it comes to CI builds, saving a few seconds here and there adds up quickly.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;overall-impressions&quot;&gt;Overall Impressions&lt;/h2&gt;

&lt;p&gt;I spent one evening setting up the workflow with the goal of getting it to feature parity with our existing CircleCI setup and I was able to achieve that.&lt;/p&gt;

&lt;p&gt;That said, I am not a DevOps expert – I’m just trying to get a solid pipeline set up so I can focus on more important things.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/features/actions&quot;&gt;GitHub Actions&lt;/a&gt; is a compelling option that is worthy of becoming the community default. It makes too much sense to have GitHub be the one-stop place for your repository, issue tracker, and build server. One less external service means less mental overheard and a more “boring” setup that lets us focus on shipping quickly and solving customer problems, not doing integration work.&lt;/p&gt;

&lt;p&gt;If I was starting a new project, I would absolutely start using GitHub Actions as my default CI service.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Feb 2020 Update&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article has been updated: the original draft was written during the GitHub Actions beta period and many of the areas for improvement (namely caching) were fixed!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mar 2021 Update&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article has been updated: switch to using built-in bundle cache options with the ruby/setup-ruby action&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;July 2022 Update&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article has been updated: switch to using built-in yarn cache options with the actions/setup-node step and bump various library versions&lt;/em&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">GitHub Actions is an automation platform that you run directly from inside a repository. We can use it as a testing CI/CD pipeline and keep everything close to the code.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/github-actions-ci.png" />
          <media:content medium="image" url="https://boringrails.com/images/github-actions-ci.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Use `to_sql` to see what query ActiveRecord will generate</title>
        <link href="https://boringrails.com/tips/active-record-to-sql" rel="alternate" type="text/html" title="Use `to_sql` to see what query ActiveRecord will generate" />
        <published>2021-03-22T13:00:00+00:00</published>
        <updated>2021-03-22T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/active-record-to-sql</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/active-record-to-sql">&lt;p&gt;If you’re trying to write a tricky ActiveRecord query that includes &lt;code class=&quot;highlighter-rouge&quot;&gt;joins&lt;/code&gt;, complex &lt;code class=&quot;highlighter-rouge&quot;&gt;where&lt;/code&gt; clauses, or selecting specific values across tables, it can be hard to remember every part of the ActiveRecord DSL.&lt;/p&gt;

&lt;p&gt;Is it &lt;code class=&quot;highlighter-rouge&quot;&gt;joins(:orders)&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;joins(:order)&lt;/code&gt;? Should you use &lt;code class=&quot;highlighter-rouge&quot;&gt;where(role: { name: &apos;Manager&apos; })&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;where(roles: { name: &apos;Manager&apos; })&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s a good idea to test these queries in the Rails console so you can quickly iterate, but sometimes you’ll be left scratching your head because when you run the code you get a weird result like:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#&amp;lt;MyModel::ActiveRecord_Relation:0x23118&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And if you try to access the results, the query blows up with a cryptic error like:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;table &lt;span class=&quot;s2&quot;&gt;&quot;permission&quot;&lt;/span&gt;
LINE 1: ....&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;permissions_users&quot;&lt;/span&gt;.&lt;span class=&quot;s2&quot;&gt;&quot;permission_id&quot;&lt;/span&gt; WHERE &lt;span class=&quot;s2&quot;&gt;&quot;permissio...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Sometimes you just want to inspect the generated SQL to debug what’s going wrong.&lt;/p&gt;

&lt;p&gt;It’s actually really easy to do this with ActiveRecord: simply call &lt;code class=&quot;highlighter-rouge&quot;&gt;to_sql&lt;/code&gt; on your query and, instead of running the SQL, it will print out the full query – even if the SQL is invalid.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email LIKE &apos;%?&apos;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;gmail.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sql&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SELECT &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.* FROM &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; WHERE (email LIKE &apos;%&apos;gmail.com&apos;&apos;)&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Aha! We messed up the &lt;code class=&quot;highlighter-rouge&quot;&gt;%?&lt;/code&gt; syntax.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email LIKE ?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%gmail.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sql&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SELECT &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.* FROM &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; WHERE (email LIKE &apos;%gmail.com&apos;)&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s especially helpful if you have multiple database tables involved.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;joins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:permissions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;permission: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;key: :edit_posts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sql&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SELECT &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.* FROM &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; INNER JOIN &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;permissions_users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; ON &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;permissions_users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; = &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; INNER JOIN &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; ON &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;permissions&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; = &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;permissions_users&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;permission_id&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; WHERE &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;permission&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; = &apos;edit_posts&apos;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Whoops! We need to use the plural &lt;code class=&quot;highlighter-rouge&quot;&gt;permissions&lt;/code&gt; in our &lt;code class=&quot;highlighter-rouge&quot;&gt;where&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;joins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:permissions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;permissions: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;key: :edit_posts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This tip has saved me countless hours of debugging complex queries. I also reach for it to validate tricky queries where I want to be sure Rails is generating the intended SQL query.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-to_sql&quot;&gt;ActiveRecord::Relation#to_sql&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Prefer returning chainable ActiveRecord objects</title>
        <link href="https://boringrails.com/tips/chainable-activerecord-queries" rel="alternate" type="text/html" title="Prefer returning chainable ActiveRecord objects" />
        <published>2021-03-18T13:00:00+00:00</published>
        <updated>2021-03-18T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-returning-chainable-objects</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/chainable-activerecord-queries">&lt;p&gt;One of the best parts about ActiveRecord is the chainable query interface:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:comments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;published: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;author: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To take advantage of this strength and give you flexibility in your code, always try to return chainable objects when querying data.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;It’s common to extract complex queries as your application grows.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SpecialOffer&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_eligible_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;restricted?&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;products&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;price &amp;gt;= ?&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;can_order?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;vi&quot;&gt;@products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SpecialOffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_eligible_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; [ #&amp;lt;Product:0x00007fb1719b7ec0&amp;gt;, #&amp;lt;Product:0x00007fb174744de8&amp;gt;, ... ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;While this code may work, what happens if you need to order the &lt;code class=&quot;highlighter-rouge&quot;&gt;@products&lt;/code&gt; in a certain way? Or add additional logic? Or lazy-load some associations?&lt;/p&gt;

&lt;p&gt;In this case, the return type of our &lt;code class=&quot;highlighter-rouge&quot;&gt;SpecialOffer&lt;/code&gt; method are arrays. We would have to switch to using Ruby array methods like &lt;code class=&quot;highlighter-rouge&quot;&gt;sort&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;select&lt;/code&gt; and maybe accidentally introduce an N+1 bug if we need more data.&lt;/p&gt;

&lt;p&gt;Let’s refactor this code to make it return chainable objects.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SpecialOffer&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_eligible_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;none&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;restricted?&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;product_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;products&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;price &amp;gt;= ?&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;can_order?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;no&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;product_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;vi&quot;&gt;@products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SpecialOffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_eligible_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; Product::ActiveRecord_Relation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;First, we make use of the &lt;code class=&quot;highlighter-rouge&quot;&gt;none&lt;/code&gt; query method: this returns an empty (but still chainable!) result. You can call ActiveRecord methods like &lt;code class=&quot;highlighter-rouge&quot;&gt;order&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;includes&lt;/code&gt;, or &lt;code class=&quot;highlighter-rouge&quot;&gt;where&lt;/code&gt; on this empty relation and it will simply return no results.&lt;/p&gt;

&lt;p&gt;Second, instead of returning the result of our complex product query directly, we collect up the right products and then return “fresh” results for just those &lt;code class=&quot;highlighter-rouge&quot;&gt;id&lt;/code&gt;s. While this does incur an additional database query, we can also manipulate the results as needed.&lt;/p&gt;

&lt;p&gt;If we want to sort the results or load an association, we can do it in the database and not be worried about any existing conditions that were run as part of the computations.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;vi&quot;&gt;@products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SpecialOffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_eligible_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:variants&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;vi&quot;&gt;@products&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SpecialOffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find_eligible_products&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shopper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;joins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:sales&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sales.count &amp;gt; 15&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:sku&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’ve found this pattern to be extremely helpful for pulling out complex queries, while still maintaining flexibility to massage the data into the correct shape.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-none&quot;&gt;ActiveRecord::QueryMethods#none&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails Docs: &lt;a href=&quot;https://guides.rubyonrails.org/active_record_querying.html&quot;&gt;Active Record Query Interface&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Rails validations: unique within a certain scope</title>
        <link href="https://boringrails.com/tips/rails-unique-scope" rel="alternate" type="text/html" title="Rails validations: unique within a certain scope" />
        <published>2021-03-16T13:00:00+00:00</published>
        <updated>2021-03-16T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-unique-scope</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-unique-scope">&lt;p&gt;It’s a great idea to make your database and application validations match. If you have &lt;code class=&quot;highlighter-rouge&quot;&gt;validates :name, presence: true&lt;/code&gt; in your model, you should pair it with a &lt;code class=&quot;highlighter-rouge&quot;&gt;not null&lt;/code&gt; database constraint. Unique validations should be paired with a &lt;code class=&quot;highlighter-rouge&quot;&gt;UNIQUE&lt;/code&gt; database index.&lt;/p&gt;

&lt;p&gt;In real-world applications, you often have more complicated validations, but you should continue this practice whenever you can.&lt;/p&gt;

&lt;p&gt;Something I encounter regularly is the need to have records that are unique, but within a certain scope.&lt;/p&gt;

&lt;p&gt;Imagine you were building a typical project management tool. You might want &lt;code class=&quot;highlighter-rouge&quot;&gt;Project&lt;/code&gt;s to have a unique name so they can be distinguished within your UI – but you don’t want the name to be globally unique. If I make a project called “Onboarding”, another customer should not be restricted from using that name as well.&lt;/p&gt;

&lt;p&gt;Luckily, Rails has got us covered with a handy feature called &lt;strong&gt;validation scopes&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;scope&lt;/code&gt; option to the Rails uniqueness validation rule allows us to specify additional columns to consider when checking for uniqueness.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Project&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:account&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:tasks&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;presence: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;uniqueness: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;scope: :account_id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This rule says that “the name of this project must unique, within the scope of this account”. In other words, the combination of a &lt;code class=&quot;highlighter-rouge&quot;&gt;name&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;account_id&lt;/code&gt; must be unique – but you can have projects with the same name in different accounts.&lt;/p&gt;

&lt;p&gt;As we discussed earlier, you really want to back-up your application level validations with database constraints.&lt;/p&gt;

&lt;p&gt;In this case, you’ll want to do a multiple column index. You can do this in a normal Rails migration.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateProject&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;6.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;change&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;create_table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:projects&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;add_index&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:projects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:account_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;unique: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;

&lt;p&gt;You can pass multiple columns to &lt;code class=&quot;highlighter-rouge&quot;&gt;scope&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you were building a dining app and wanted to enforce that a guest could only have one reservation at a restaurant per day.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Reservation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:guest&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:restaurant&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:guest_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;uniqueness: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;scope: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:restaurant_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:reservation_date&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You may wish to change the message since the defaults error message will be fairly spartan: “{field} has already been taken”&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:guest_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;uniqueness: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;scope: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:restaurant_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:reservation_date&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;message: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Only one reservation per guest per day is permitted&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note: In PostgreSQL, the default limit for index names is 63 characters so you may find yourself needing to change the index name if your model or column names are longer.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;add_index&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:reservations&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:guest_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:restaurant_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:reservation_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;unique: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;idx_reserveration_guest_date_uniq&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of&quot;&gt;Uniqueness Validations&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL Docs: &lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-constraints.html&quot;&gt;Postgres Constraints&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MySql Docs: &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/multiple-column-indexes.html&quot;&gt;Multi-column Indexes&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Boring breadcrumbs for Rails</title>
        <link href="https://boringrails.com/tips/boring-breadcrumbs-rails" rel="alternate" type="text/html" title="Boring breadcrumbs for Rails" />
        <published>2021-03-12T13:00:00+00:00</published>
        <updated>2021-03-12T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/boring-breadcrumbs-for-rails</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/boring-breadcrumbs-rails">&lt;p&gt;Breadcrumbs are a common UI pattern in most software applications. Rails has no built-in tools specifically for breadcrumbs, and while &lt;a href=&quot;https://github.com/fnando/breadcrumbs&quot;&gt;there&lt;/a&gt; &lt;a href=&quot;https://github.com/piotrmurach/loaf&quot;&gt;are a&lt;/a&gt; &lt;a href=&quot;https://github.com/lassebunk/gretel&quot;&gt;handful&lt;/a&gt; &lt;a href=&quot;https://github.com/zachinglis/crummy&quot;&gt;of existing&lt;/a&gt; &lt;a href=&quot;https://github.com/weppos/breadcrumbs_on_rails&quot;&gt;gems&lt;/a&gt;, I think this is something you can easily implement in your own app with just a few lines of code.&lt;/p&gt;

&lt;p&gt;Ultimately, you’ll want control of how you display the breadcrumbs in your app so you might as well just own all the code for this functionality.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;First, add a model for a &lt;code class=&quot;highlighter-rouge&quot;&gt;Breadcrumb&lt;/code&gt;. This can live in the &lt;code class=&quot;highlighter-rouge&quot;&gt;app/models&lt;/code&gt; folder but does not need to be an ActiveRecord model since we don’t need it to persist to the database.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Breadcrumb&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;attr_reader&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:path&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;link?&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present?&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next add a method to your &lt;code class=&quot;highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt; to store and add breadcrumbs.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApplicationController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;helper_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:breadcrumbs&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;breadcrumbs&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@breadcrumbs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;add_breadcrumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;breadcrumbs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Breadcrumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then in your layout, you can render the breadcrumbs however you’d like. In my applications, I use the breadcrumbs in both the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;title&amp;gt;&lt;/code&gt; head tag and in the page header.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;title&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;breadcrumbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reverse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;My App&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; | &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;ol&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;breadcrumbs&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;breadcrumbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crumb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
     &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;link?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;breadcrumb-link&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;breadcrumb-page&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crumb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;breadcrumbs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;last&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;breadcrumb-separator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;/&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
     &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s a simple API, but this model will allow you to add breadcrumbs in each of your controllers. The breadcrumb can include an optional &lt;code class=&quot;highlighter-rouge&quot;&gt;path&lt;/code&gt; if you want it to be a link.&lt;/p&gt;

&lt;p&gt;You can setup breadcrumbs in &lt;code class=&quot;highlighter-rouge&quot;&gt;before_actions&lt;/code&gt; or inside each action. You can add conditional logic just like with any regular Ruby code.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set_breadcrumbs&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@posts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;add_breadcrumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@post&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;add_breadcrumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;New Post&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;set_breadcrumbs&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add_breadcrumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Admin&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;admin_home_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;admin?&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add_breadcrumb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Posts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Sexy? No. Boring? Yes. Works great? Of course.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Sharing common code between Rails controllers with `Scoped` pattern</title>
        <link href="https://boringrails.com/tips/rails-scoped-controllers-sharing-code" rel="alternate" type="text/html" title="Sharing common code between Rails controllers with `Scoped` pattern" />
        <published>2021-03-10T13:00:00+00:00</published>
        <updated>2021-03-10T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-scoped-controllers-sharing-code</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-scoped-controllers-sharing-code">&lt;p&gt;If you follow a strict REST / nested resources approach to building your Rails app, you might get sick of repeating common controller actions.&lt;/p&gt;

&lt;p&gt;Try the &lt;code class=&quot;highlighter-rouge&quot;&gt;Scoped&lt;/code&gt; concern pattern: a place to put shared code (setting variables, authorization) and slim down your controllers.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;This particular pattern comes from DHH and Basecamp – a codebase that p&lt;a href=&quot;https://twitter.com/dhh/status/964244090224128001&quot;&gt;rides itself of using lots of tiny concerns&lt;/a&gt; to share bits of behavior.&lt;/p&gt;

&lt;p&gt;While the savings of repeating the same &lt;code class=&quot;highlighter-rouge&quot;&gt;before_action&lt;/code&gt;s to look up a &lt;code class=&quot;highlighter-rouge&quot;&gt;Channel&lt;/code&gt; would be a fine benefit on it’s own, the naming convention of &lt;code class=&quot;highlighter-rouge&quot;&gt;Scoped&lt;/code&gt; is such a great, sharp name. Playlists are “scoped” to a channel so it makes total sense that the corresponding controller would be “channel scoped”.&lt;/p&gt;

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

  &lt;span class=&quot;n&quot;&gt;included&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set_channel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:authorize_channel&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;set_channel&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@channel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Channel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:channel_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;authorize_channel&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;authorize&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@channel&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# check that user has access, etc&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Channels::SubscriptionsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ChannelScoped&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Channels::VideosController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ChannelScoped&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Channels::PlaylistsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ChannelScoped&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;DHH Gist: &lt;a href=&quot;https://gist.github.com/dhh/10022098&quot;&gt;Models for Nested Resources&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Run different ActiveRecord validations based on context</title>
        <link href="https://boringrails.com/tips/activerecord-validation-context" rel="alternate" type="text/html" title="Run different ActiveRecord validations based on context" />
        <published>2021-03-08T13:00:00+00:00</published>
        <updated>2021-03-08T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-validate-context</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/activerecord-validation-context">&lt;p&gt;Sometimes want to skip certain validations on your database models. Maybe you have a multi-step wizard or want admins to have more freedom in changing data.&lt;/p&gt;

&lt;p&gt;You might be tempted to have certain forms skip validations, but there is a better way.&lt;/p&gt;

&lt;p&gt;Rails allows you to pass in a &lt;code class=&quot;highlighter-rouge&quot;&gt;context&lt;/code&gt; when saving or validating a record. You can combine &lt;code class=&quot;highlighter-rouge&quot;&gt;context&lt;/code&gt; with the &lt;code class=&quot;highlighter-rouge&quot;&gt;on:&lt;/code&gt; option to run only certain ActiveRecord validations.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Let’s say you want want to build a multi-step workflow for a job board. You might allow someone to create a job listing and start filling in the data, but not fully validate everything until it is time to publish the listing.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Listing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:company&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:user&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;has_rich_text&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:requirements&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;presence: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;length: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;maximum: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:salary_range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;presence: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;on: :publish&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:application_instructions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;presence: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;on: :publish&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;publish!&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;published_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;context: :publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this case, we will always require a &lt;code class=&quot;highlighter-rouge&quot;&gt;title&lt;/code&gt; (with 50 characters max) whenever we create or edit this record. But we will only validate &lt;code class=&quot;highlighter-rouge&quot;&gt;salary_range&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;application_instructions&lt;/code&gt; when we pass &lt;code class=&quot;highlighter-rouge&quot;&gt;:publish&lt;/code&gt; as the validation context.&lt;/p&gt;

&lt;p&gt;You could implement this workflow with controller actions like:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ListingsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@listing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;listing_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;notice: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Listing created&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: :unprocessable_entity&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;publish&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@listing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publish!&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;notice: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Listing published&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:edit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: :unprocessable_entity&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also add validations that are different based on which user is making the change. Maybe you want to allow admins to give special, short usernames to your friends.&lt;/p&gt;

&lt;p&gt;Here we can set one rule that requires six character usernames in the &lt;code class=&quot;highlighter-rouge&quot;&gt;:create&lt;/code&gt; context (which Rails will include by default when creating a record). Then we add a rule in the &lt;code class=&quot;highlighter-rouge&quot;&gt;:admin&lt;/code&gt; context that only requires three characters.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Account&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;length: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;minimum: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;on: :create&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:username&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;length: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;minimum: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;on: :admin&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;username: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;swanson&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;valid?&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;username: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;swanson&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;valid?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; true&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;username: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mds&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;valid?&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;username: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mds&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;valid?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; true&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;username: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;valid?&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; false&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;username: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;valid?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:admin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# =&amp;gt; false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One downside with using Rails validation contexts is that you may not be able to use database level validations. Being able to persist partially-valid records or have conditional rules is a powerful feature, but it’s not without costs.&lt;/p&gt;

&lt;p&gt;Think carefully about moving validations from the database constraint level to your application.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails Guides: &lt;a href=&quot;https://guides.rubyonrails.org/active_record_validations.html#on&quot;&gt;Validations :on option&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/Validations.html#method-i-valid-3F&quot;&gt;ActiveRecord::Validations#valid?&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Find records missing an association with `where.missing`</title>
        <link href="https://boringrails.com/tips/activerecord-where-missing-associations" rel="alternate" type="text/html" title="Find records missing an association with `where.missing`" />
        <published>2021-03-04T13:00:00+00:00</published>
        <updated>2021-03-04T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/activerecord-find-missing-associations</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/activerecord-where-missing-associations">&lt;p&gt;You can’t prove a negative, but what about querying a database for a negative? While the majority of the time you are writing queries to find data, there are some cases when you want the opposite: writing a query that looks for the absence of data.&lt;/p&gt;

&lt;p&gt;When it comes to raw SQL, you can use a &lt;code class=&quot;highlighter-rouge&quot;&gt;LEFT OUTER JOIN&lt;/code&gt; combined with a &lt;code class=&quot;highlighter-rouge&quot;&gt;NULL&lt;/code&gt; check to find records without certain associations.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;In Rails, you can apply the same concepts from SQL directly with ActiveRecord.&lt;/p&gt;

&lt;p&gt;Let’s say you have the following model:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Account&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:recovery_email_addresses&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you wanted to find &lt;code class=&quot;highlighter-rouge&quot;&gt;Account&lt;/code&gt;s that have not yet set up a backup recovery email, you could write this query and everything will work just fine:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;left_joins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:recovery_email_addresses&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;recovery_email_addresses: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# SELECT &quot;accounts&quot;.* FROM &quot;accounts&quot; LEFT OUTER JOIN &quot;recovery_email_addresses&quot; ON &quot;recovery_email_addresses&quot;.&quot;account_id&quot; = &quot;accounts&quot;.&quot;id&quot; WHERE &quot;recovery_email_addresses&quot;.&quot;id&quot; IS NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But it’s kind of verbose. Since Rails 6.1, you can use a much cleaner shorthand to write this same query.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Account&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;missing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:recovery_email_addresses&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# SELECT &quot;accounts&quot;.* FROM &quot;accounts&quot; LEFT OUTER JOIN &quot;recovery_email_addresses&quot; ON &quot;recovery_email_addresses&quot;.&quot;account_id&quot; = &quot;accounts&quot;.&quot;id&quot; WHERE &quot;recovery_email_addresses&quot;.&quot;id&quot; IS NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will generate identical SQL and is a lot easier to read. You can use this functionality on &lt;code class=&quot;highlighter-rouge&quot;&gt;belongs_to&lt;/code&gt; relationships as well.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Contract&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:promiser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class_name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:promisee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class_name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:beneficiary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;optional: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class_name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Contract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;missing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:promiser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Contracts without a promiser&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Contract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;missing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:promiser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:beneficiary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Contracts without a promiser AND beneficiary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also combine &lt;code class=&quot;highlighter-rouge&quot;&gt;missing&lt;/code&gt; with your normal ActiveRecord chaining methods&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Contact&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;amount &amp;gt; ?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;missing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:promiser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Contact&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;signed: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;missing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:beneficiary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails repository: &lt;a href=&quot;https://github.com/rails/rails/pull/34727&quot;&gt;Finding Orphan Records &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API docs: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-missing&quot;&gt;WhereChain#missing&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Testing multiple sessions in the same test with Capybara</title>
        <link href="https://boringrails.com/tips/capybara-multiple-user-sessions" rel="alternate" type="text/html" title="Testing multiple sessions in the same test with Capybara" />
        <published>2021-03-02T13:00:00+00:00</published>
        <updated>2021-03-02T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/capybara-testing-multiple-sessions</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/capybara-multiple-user-sessions">&lt;p&gt;Sometimes a feature in your application will involve a back-and-forth between multiple users. When it comes time to write an automated system test, you can easily simulate switching between users using Capybara’s &lt;code class=&quot;highlighter-rouge&quot;&gt;using_session&lt;/code&gt; helper.&lt;/p&gt;

&lt;p&gt;Instead of logging in and out or faking out another user making changes to the app, you can use multiple sessions within the same Capybara test.&lt;/p&gt;

&lt;p&gt;This can be very useful for testing features like notifications, chat, or even multi-person workflows where different users have to take action to move a process forward.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;There are a few options for controlling the session in Capybara.&lt;/p&gt;

&lt;p&gt;You can set the session manually:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Capybara&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;session_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Test session #1&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But I prefer the &lt;code class=&quot;highlighter-rouge&quot;&gt;using_session&lt;/code&gt; block helper, which will run any code inside the block in a separate session and then revert back to the original session when you leave the block.&lt;/p&gt;

&lt;p&gt;Here is an example of how you could test a basic task management system where users can assign tasks to others to complete.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;describe&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Task Assignment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;type: :system&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;it&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;allows users to assign tasks to other users&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;login&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:kelly&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/tasks&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;click_on&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Review deliverable&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;click_button&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Assign to...&quot;&lt;/span&gt;

    &lt;span class=&quot;nb&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Sam&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;from: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Assignee&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;click_button&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Save&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Status: Pending&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Assigned: Sam&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;using_session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Sam&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;login&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:sam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/tasks/me&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Review deliverable&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;click_on&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Review deliverable&quot;&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;click_on&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Mark complete&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;refresh&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;expect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;have_content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Status: Completed&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Capybara Docs: &lt;a href=&quot;https://github.com/teamcapybara/capybara#using-sessions&quot;&gt;Using Multiple Sessions&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        
          <category term="testing" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Pluck single values out of ActiveRecord models or Enumerables</title>
        <link href="https://boringrails.com/tips/rails-pluck-single-values" rel="alternate" type="text/html" title="Pluck single values out of ActiveRecord models or Enumerables" />
        <published>2021-02-26T13:00:00+00:00</published>
        <updated>2021-02-26T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/plucking-values-enumerables-activerecord</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-pluck-single-values">&lt;p&gt;Rails has a great, expressive term called &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt; that allows you to grab a subset of data from a record. You can use this on ActiveRecord models to return one (or a few) columns.&lt;/p&gt;

&lt;p&gt;But you can also use the same method on regular old &lt;code class=&quot;highlighter-rouge&quot;&gt;Enumerables&lt;/code&gt; to pull out all values that respond to a given key.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;In Rails, use &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt; to query a subset of columns.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# SELECT &quot;shoes.*&quot; from &quot;shoes&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [&quot;Air Force 1&quot;, &quot;NMD_2&quot;, &quot;Air Jordans&quot;, ... ]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# This returns an array with all shoe names, but our database query pulled down all of the columns on the `shoes` table&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pluck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# SELECT &quot;shoes.name&quot; from &quot;shoes&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [&quot;Air Force 1&quot;, &quot;NMD_2&quot;, &quot;Air Jordans&quot;, ... ]&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Same result, but we only query exactly the columns we wanted&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pluck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# SELECT &quot;shoes&quot;.&quot;name&quot;, &quot;shoes&quot;.&quot;brand&quot; FROM &quot;shoes&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [[&quot;Air Jordan 1 Mid&quot;, &quot;Nike&quot;], [&quot;Air Jordan 1 Mid&quot;, &quot;Nike&quot;], ... ]&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;Shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;distinct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pluck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# SELECT DISTINCT &quot;shoes&quot;.&quot;brand&quot; FROM &quot;shoes&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [&quot;Nike&quot;, &quot;Adidas&quot;, ... ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also use &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;Enumerables&lt;/code&gt; when using &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveSupport&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;David&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Rafael&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Aaron&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pluck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [ &quot;David&quot;, &quot;Rafael&quot;, &quot;Aaron&quot; ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I find the &lt;code class=&quot;highlighter-rouge&quot;&gt;Enumerable&lt;/code&gt; version to be particularly handy when dealing with JSON data from external APIs.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;httparty&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;active_support&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;active_support/core_ext&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;HTTParty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;http://api.stackexchange.com/2.2/questions?site=stackoverflow&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;questions&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;items&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;questions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;pluck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; [&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   &quot;JavaScript to Python - Interpreting JavasScript .filter() to a Python user&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   &quot;Nuxt generate and firebase gives timer warning&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   &quot;Variable expected error when I increment the value of a map&quot;,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#   ...&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;While &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt; is most often used with Hashes, you can use it with any object that responds to the message you pass in – including regular Ruby objects or Structs.&lt;/p&gt;

&lt;p&gt;Next time you find yourself calling &lt;code class=&quot;highlighter-rouge&quot;&gt;map&lt;/code&gt; to get back a single value, see if your code might be improved by switching to &lt;code class=&quot;highlighter-rouge&quot;&gt;pluck&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-pluck&quot;&gt;ActiveRecord#pluck&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_support_core_extensions.html#pluck&quot;&gt;Enumerable#pluck&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Never mix up greater/less than when comparing dates again</title>
        <link href="https://boringrails.com/tips/rails-date-before-after" rel="alternate" type="text/html" title="Never mix up greater/less than when comparing dates again" />
        <published>2021-02-24T13:00:00+00:00</published>
        <updated>2021-02-24T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-date-before-after</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-date-before-after">&lt;p&gt;When it comes to compare dates, for some reason my brain really struggles. I mix up &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;gt;=&lt;/code&gt; all the time and end up flipping them.&lt;/p&gt;

&lt;p&gt;Is &lt;code class=&quot;highlighter-rouge&quot;&gt;start_date&lt;/code&gt; greater than &lt;code class=&quot;highlighter-rouge&quot;&gt;end_date&lt;/code&gt;? Or vice-versa? I get confused because I think about dates in terms of &lt;code class=&quot;highlighter-rouge&quot;&gt;before&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;after&lt;/code&gt; not &lt;code class=&quot;highlighter-rouge&quot;&gt;greater_than&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;less_than&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Luckily, Rails is here to save the day and make sure I never make this mistake again by adding &lt;code class=&quot;highlighter-rouge&quot;&gt;before?&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;after?&lt;/code&gt; for all Date-related comparisons.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;start_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2019&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;31&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;end_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2019&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;before?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end_date&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; true&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;end_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;after?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_date&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; true&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;start_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2020&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;end_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2018&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;before?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end_date&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I find myself using these methods frequently in Rails model validations when I need to ensure two dates form a valid range.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Promotion&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;validate&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:valid_eligiblity_range?&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;valid_eligiblity_range?&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expiration_date?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start_date?&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expiration_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;after?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;must be before Expiration Date&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:expiration_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;must be after Start Date&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://api.rubyonrails.org/classes/DateAndTime/Calculations.html#method-i-before-3F&quot;&gt;DateAndTime::Calculations#before?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://api.rubyonrails.org/classes/DateAndTime/Calculations.html#method-i-after-3F&quot;&gt;DateAndTime::Calculations#after?&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Super readable String operations with `delete_prefix` and `delete_suffix`</title>
        <link href="https://boringrails.com/tips/ruby-delete-prefix-suffix" rel="alternate" type="text/html" title="Super readable String operations with `delete_prefix` and `delete_suffix`" />
        <published>2021-02-22T13:00:00+00:00</published>
        <updated>2021-02-22T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/ruby-delete-prefix-suffix</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/ruby-delete-prefix-suffix">&lt;p&gt;One reason I love writing Ruby is that it’s optimized for programmer happiness. The Ruby community values code that is super readable.&lt;/p&gt;

&lt;p&gt;Programmers coming from other ecosystems are often shocked at much Ruby looks like pseudo-code. Between the standard library and extensions like &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveSupport&lt;/code&gt;, working with Ruby means you can write code in a natural way.&lt;/p&gt;

&lt;p&gt;A great example of this are the String methods &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_prefix&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_suffix&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;You can use &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_prefix&lt;/code&gt; to, as the name says, delete a substring from the beginning of a string.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s2&quot;&gt;&quot;BoringRails!&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete_prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Boring&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;Rails!&quot;&lt;/span&gt;

&lt;span class=&quot;s2&quot;&gt;&quot;#programming&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete_prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;#&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;programming&quot;&lt;/span&gt;

&lt;span class=&quot;s2&quot;&gt;&quot;ISBN: 9780091929787&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete_prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ISBN: &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;9780091929787&quot;&lt;/span&gt;

&lt;span class=&quot;s2&quot;&gt;&quot;mailto:test@example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete_prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mailto:&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;test@example.com&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;github_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://github.com/rails/rails&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;repo_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;github_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete_prefix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://github.com/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;rails/rails&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Compare this method to other options like &lt;code class=&quot;highlighter-rouge&quot;&gt;gsub&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;github_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://github.com/rails/rails&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;repo_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;github_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;gsub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://github.com/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;rails/rails&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;While &lt;code class=&quot;highlighter-rouge&quot;&gt;gsub&lt;/code&gt; is more flexible (it can take a Regex and swap multiple occurences), the code doesn’t read as naturally as &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_prefix&lt;/code&gt;. And &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_prefix&lt;/code&gt; is &lt;a href=&quot;https://docs.rubocop.org/rubocop-performance/cops_performance.html#performancedeleteprefix&quot;&gt;more performant&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;If you want to remove characters at the end of a string, you have two main options: &lt;code class=&quot;highlighter-rouge&quot;&gt;chomp&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_suffix&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;s2&quot;&gt;&quot;report.csv&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;chomp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;.csv&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;report&quot;&lt;/span&gt;

&lt;span class=&quot;s2&quot;&gt;&quot;report.csv&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete_suffix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;.csv&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; &quot;report&quot;&lt;/span&gt;

&lt;span class=&quot;s2&quot;&gt;&quot;150 cm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete_suffix&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;cm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#=&amp;gt; 150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Stylistically, &lt;code class=&quot;highlighter-rouge&quot;&gt;chomp&lt;/code&gt; has a bit more of the whimsy that you might expect from Ruby, while &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_suffix&lt;/code&gt; is a nice mirroring of &lt;code class=&quot;highlighter-rouge&quot;&gt;delete_prefix&lt;/code&gt; and more explicitly named. Both have similar &lt;a href=&quot;https://github.com/JuanitoFatas/fast-ruby#stringsub-vs-stringchomp-vs-stringdelete_suffix-code&quot;&gt;performance benchmarks&lt;/a&gt; and are faster than &lt;code class=&quot;highlighter-rouge&quot;&gt;sub&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Ruby API Docs: &lt;a href=&quot;https://ruby-doc.org/core-2.5.1/String.html#method-i-delete_prefix&quot;&gt;String#delete_prefix&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ruby API Docs: &lt;a href=&quot;https://ruby-doc.org/core-2.5.1/String.html#method-i-delete_suffix&quot;&gt;String#delete_suffix&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ruby Bug Tracker: &lt;a href=&quot;https://bugs.ruby-lang.org/issues/12694&quot;&gt;Feature discussion&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;String method benchmarks: &lt;a href=&quot;https://github.com/JuanitoFatas/fast-ruby#string&quot;&gt;Fast Ruby - String&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Setting CSS classes in Markdown with Jekyll / Bridgetown</title>
        <link href="https://boringrails.com/tips/jekyll-css-class" rel="alternate" type="text/html" title="Setting CSS classes in Markdown with Jekyll / Bridgetown" />
        <published>2021-02-19T13:00:00+00:00</published>
        <updated>2021-02-19T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/jekyll-css-class</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/jekyll-css-class">&lt;p&gt;Writing blog posts in Markdown is just great. This blog is written in Markdown!&lt;/p&gt;

&lt;p&gt;But sometimes you might be tempted to drop down to raw HTML to add some extra styling.&lt;/p&gt;

&lt;p class=&quot;pro-tip&quot;&gt;For example maybe you want to write this content in markdown but have it apply a “pro-tip” CSS class so that it looks like…well, this!&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Popular Ruby static site generators like &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;Jekyll&lt;/a&gt; and &lt;a href=&quot;https://www.bridgetownrb.com/&quot;&gt;Bridgetown&lt;/a&gt; use &lt;a href=&quot;https://kramdown.gettalong.org/&quot;&gt;Kramdown&lt;/a&gt; under-the-hood to render your Markdown by default.&lt;/p&gt;

&lt;p&gt;In Kramdown, there is a feature called “Block Inline Attribute Lists”, which is an extension of standard Markdown syntax.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;A simple paragraph with an ID attribute.
{: #para-one}

&amp;gt; A blockquote with a title
{: .pull-quote }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You use &lt;code class=&quot;highlighter-rouge&quot;&gt;{: CSS_SELECTOR }&lt;/code&gt; to attach additional HTML attributes to the rendered Markdown output.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/jekyll-css.jpeg&quot; alt=&quot;Jekyll CSS classes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s great for adding small embellishments to your posts without having to drop down to raw HTML.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Kramdown Doc: &lt;a href=&quot;https://kramdown.gettalong.org/syntax.html#block-ials&quot;&gt;Inline Attribute Lists&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Use Heroku Dataclips to share query and do ad-hoc data exports</title>
        <link href="https://boringrails.com/tips/heroku-data-clips" rel="alternate" type="text/html" title="Use Heroku Dataclips to share query and do ad-hoc data exports" />
        <published>2021-02-18T13:00:00+00:00</published>
        <updated>2021-02-18T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/heroku-data-clips</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/heroku-data-clips">&lt;p&gt;&lt;a href=&quot;https://devcenter.heroku.com/articles/dataclips&quot;&gt;Heroku Dataclips&lt;/a&gt; enable you to create SQL queries for your Heroku Postgres databases and share the results with colleagues, third-party tools, and the public. Recipients of a dataclip can view the data in their browser and also download it in JSON and CSV formats.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/data-clips.jpeg&quot; alt=&quot;Heroku Dataclips&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;If you are hosting your app on Heroku, you might need to run some ad-hoc queries or share a report. Instead of generating an admin page or a CSV report, you can make a Dataclip that attaches to your production database and run raw SQL queries.&lt;/p&gt;

&lt;p&gt;The editor has autocomplete features for tables and columns and you can export the data to CSV or JSON.&lt;/p&gt;

&lt;p&gt;Super handy for one-off SQL queries or barebones reporting and way better than having to &lt;code class=&quot;highlighter-rouge&quot;&gt;rails c&lt;/code&gt; or dig up the raw connection string!&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Heroku Blog: &lt;a href=&quot;https://blog.heroku.com/dialog-with-data-new-dataclips&quot;&gt;A Dialog with Your Data Using the New Dataclips&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Heroku Blog: &lt;a href=&quot;https://blog.heroku.com/new-dataclips&quot;&gt;Share your Heroku Postgres data with the new Dataclips&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="product" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Ensure required environment variables are set when booting up Rails</title>
        <link href="https://boringrails.com/tips/ensure-rails-env-vars" rel="alternate" type="text/html" title="Ensure required environment variables are set when booting up Rails" />
        <published>2021-02-17T13:00:00+00:00</published>
        <updated>2021-02-17T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/ensure-rails-env-vars</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/ensure-rails-env-vars">&lt;p&gt;It’s common to use &lt;a href=&quot;https://12factor.net/config&quot;&gt;environment variables to configure&lt;/a&gt; external services or other options in a Rails app. These &lt;code class=&quot;highlighter-rouge&quot;&gt;ENV_VARS&lt;/code&gt; usually are not checked into source control, but rather configured per environment.&lt;/p&gt;

&lt;p&gt;Rails has the concept of &lt;code class=&quot;highlighter-rouge&quot;&gt;initializers&lt;/code&gt;, which is code run during the boot phase of a Rails app.&lt;/p&gt;

&lt;p&gt;You can add a custom &lt;code class=&quot;highlighter-rouge&quot;&gt;initializer&lt;/code&gt; to check that required environment variables are set to avoid exceptions later on when your code expects a value to exist.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Create a new initializer in your app and add the required variables:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/initializers/01_ensure_environment.rb&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;development?&lt;/span&gt;
  &lt;span class=&quot;sx&quot;&gt;%w[
    AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY
    S3_BUCKET
    ALGOLIA_ID
    ALGOLIA_API_KEY
    ALGOLIA_SEARCH_KEY
    ALGOLIA_INDEX
    ALGOLIA_CAMPAIGN_INDEX
    TWITTER_API_SECRET
    TWITTER_API_TOKEN
  ]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env_var&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;has_key?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env_var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env_var&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;blank?&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOL&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
      Missing environment variable: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env_var&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;

      Ask a teammate for the appropriate value.
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;      EOL&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;

&lt;p&gt;Rails initializers are loaded and executed in alphabetical order. So use a name like &lt;code class=&quot;highlighter-rouge&quot;&gt;01_ensure_environment.rb&lt;/code&gt; to control the sort order and make sure this one loads first.&lt;/p&gt;

&lt;p&gt;You may wish to check in a sample &lt;code class=&quot;highlighter-rouge&quot;&gt;.env.sample&lt;/code&gt; file into git (without any values) to make it easier for new team members to get their environment into a working state.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails Doc: &lt;a href=&quot;https://guides.rubyonrails.org/configuring.html&quot;&gt;Configuring Rails apps&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Search and debug gems with `bundle open`</title>
        <link href="https://boringrails.com/tips/bundle-open-debug-gems" rel="alternate" type="text/html" title="Search and debug gems with `bundle open`" />
        <published>2021-02-16T13:00:00+00:00</published>
        <updated>2021-02-16T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/bundle-open</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/bundle-open-debug-gems">&lt;p&gt;Ever get frustrated trying to search through code on GitHub? Or wish you could put a breakpoint in a gem so you could figure out what it was doing?&lt;/p&gt;

&lt;p&gt;Don’t mess around with cloning the gem repo or monkey patching code in your own app. Use &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle open&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/bundle-open.gif&quot; alt=&quot;Example of bundle open command&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;In your shell, run the command: &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle open GEM_NAME&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;bundler&lt;/code&gt; will open the source code for the exact version of the gem you’ve got installed in your editor. You can search the code inside your editor and even add breakpoints or make code changes locally to test out things.&lt;/p&gt;

&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle open&lt;/code&gt; command launches the editor that you’ve set via the &lt;code class=&quot;highlighter-rouge&quot;&gt;EDITOR&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;BUNDLER_EDITOR&lt;/code&gt; environment variables. You can set this to be &lt;code class=&quot;highlighter-rouge&quot;&gt;vim&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;emacs&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;VS Code&lt;/code&gt;, or whatever tool you like to use.&lt;/p&gt;

&lt;p&gt;If you’ve made changes to the local gems for debugging or experimental purposes, you can use the &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle pristine&lt;/code&gt; command to restore your gems to their “original” state.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Bundler Docs: &lt;a href=&quot;https://bundler.io/bundle_open.html&quot;&gt;bundle open&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bundler Docs: &lt;a href=&quot;https://bundler.io/v2.2/man/bundle-pristine.1.html&quot;&gt;bundle pristine&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Automatically cast params with the Rails Attributes API</title>
        <link href="https://boringrails.com/tips/rails-attributes-api" rel="alternate" type="text/html" title="Automatically cast params with the Rails Attributes API" />
        <published>2021-02-15T13:00:00+00:00</published>
        <updated>2021-02-15T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-attributes-api</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-attributes-api">&lt;p&gt;A common practice in Rails apps is to extract logic into plain-old Ruby objects (POROs). But often you are passing data to these objects directly from controller &lt;code class=&quot;highlighter-rouge&quot;&gt;params&lt;/code&gt; and the data comes in as strings.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SalesReport&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;attr_accessor&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:min_items&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@start_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@end_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@min_items&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:min_items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run!&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Do some cool stuff&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;report&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SalesReport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;start_date: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2020-01-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;end_date: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2020-03-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;min_items: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;10&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# But the data is just stored as strings :(&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_date&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; &quot;2020-01-01&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;min_items&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; &quot;10&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You probably want &lt;code class=&quot;highlighter-rouge&quot;&gt;start_date&lt;/code&gt; to be a date and &lt;code class=&quot;highlighter-rouge&quot;&gt;min_items&lt;/code&gt; to be an integer. You could add your own basic type casting to the constructor.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SalesReport&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;attr_accessor&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:min_items&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@start_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@end_date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;err&quot;&gt; &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@min_items&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:min_items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run!&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Do some cool stuff&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But even better, you could take advantage of the &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute&quot;&gt;Attributes API&lt;/a&gt; to handle this casting automatically.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;The Rails Attributes API is used under-the-hood to type cast attributes for &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord&lt;/code&gt; models. When you query for a model that has a &lt;code class=&quot;highlighter-rouge&quot;&gt;datetime&lt;/code&gt; column in the database and the Ruby object that gets pulled out has a &lt;code class=&quot;highlighter-rouge&quot;&gt;DateTime&lt;/code&gt; field – that’s the Attributes API at work.&lt;/p&gt;

&lt;p&gt;We can spruce up our report model by mixing in the &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveModel::Model&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveModel::Attributes&lt;/code&gt; modules.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SalesReport&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Model&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveModel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Attributes&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:date&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:end_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:date&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:min_items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:integer&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run!&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Do some cool stuff&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;report&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SalesReport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;start_date: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2020-01-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;end_date: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2020-03-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;min_items: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;10&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Now the attributes are native types!&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start_date&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Wed, 01 Jan 2020&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;min_items&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; 10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This pattern is great for reducing boilerplate code in form objects, report objects, or any other Model-ish Ruby class in your Rails apps. Let the framework do the type casting for you, instead of trying to reimplement it yourself!&lt;/p&gt;

&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;

&lt;p class=&quot;pro-tip&quot;&gt;As of Rails 6.1, this module is technically a private API. Use at your own risk!&lt;/p&gt;

&lt;p&gt;The Attribute API will automatically handle type casting for most primitives. All of the basics are covered.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:date&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:max_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:integer&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:boolean&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:float&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can find the full list of out-of-the-box types here: &lt;a href=&quot;https://github.com/rails/rails/tree/v6.0.2.1/activemodel/lib/active_model/type&quot;&gt;activemodel/lib/active_model/type&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The coolest part is that the types are very robust in what kind of input they accept. For example, the boolean Attribute type works with any of these values for &lt;code class=&quot;highlighter-rouge&quot;&gt;false&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;FALSE_VALUES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;f&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;F&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;false&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;FALSE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:FALSE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;off&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:off&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;OFF&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:OFF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also register your own custom types that implement &lt;code class=&quot;highlighter-rouge&quot;&gt;cast&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;serialize&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;register&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:zip_code&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ZipCodeType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ZipCodeType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Value&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ZipCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# cast to your own ZipCode class for special handling&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;serialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Additionally, you can set a default value for with the Attributes API:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:start_date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;days&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;ago&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:max_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:enabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:boolean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;attribute&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:float&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;default: &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;9.75&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute&quot;&gt;Attributes API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blog post: &lt;a href=&quot;https://blog.dario-hamidi.de/a/rails-hidden-type-system&quot;&gt;Rails’ hidden type system&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Show relevant chunks of text with Rails `excerpt` helper</title>
        <link href="https://boringrails.com/tips/rails-excerpt-helper" rel="alternate" type="text/html" title="Show relevant chunks of text with Rails `excerpt` helper" />
        <published>2021-02-12T13:00:00+00:00</published>
        <updated>2021-02-12T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-excerpt-helper</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-excerpt-helper">&lt;p&gt;The Rails helper &lt;code class=&quot;highlighter-rouge&quot;&gt;excerpt&lt;/code&gt; can extract a chunk of text that matches a certain phrase, no matter where in the string it is.&lt;/p&gt;

&lt;p&gt;Imagine you wanted to display a list of emails matching a certain search term:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/excerpt-example.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Simply filter down your records and then use &lt;code class=&quot;highlighter-rouge&quot;&gt;excerpt&lt;/code&gt; on the email body.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Here’s what your view might look like to build this feature.&lt;/p&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;&amp;lt;%= excerpt(email.body.to_plain_text, params[:q], radius: 15) %&amp;gt;&quot; /&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex justify-between items-center&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-sm font-medium text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sender_name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;local_time_ago&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;received_at&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-sm text-gray-700&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;subject&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-sm text-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_plain_text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:q&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One trick is that if you are using &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionText&lt;/code&gt;, call &lt;code class=&quot;highlighter-rouge&quot;&gt;to_plain_text&lt;/code&gt; otherwise you might end up with unclosed HTML tags. In general, when you show an excerpt of rich text, you don’t want all of the formatting to be applied anyways.&lt;/p&gt;

&lt;p class=&quot;pro-tip&quot;&gt;For bonus points, combine with the &lt;a href=&quot;https://boringrails.com/tips/rails-highlight-search-results&quot;&gt;Rails highlight helper&lt;/a&gt;!&lt;/p&gt;

&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;A one-of-a-kind vintange DHH coffee mug signed by the man himself!&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;coffee&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;...tange DHH coffee mug signe...&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This helper is case-insensitive and can accept a string or Regex:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;A one-of-a-kind vintange DHH coffee mug signed by the man himself!&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;COFFEE&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;...tange DHH coffee mug signe...&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/tea|coffee/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;...tange DHH coffee mug signe...&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;options-1&quot;&gt;Options&lt;/h2&gt;

&lt;p&gt;You can expand the “radius” – or how many characters around the match – are shown. The default is &lt;code class=&quot;highlighter-rouge&quot;&gt;100&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;A one-of-a-kind vintange DHH coffee mug signed by the man himself!&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;coffee&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;...DHH coffee mug...&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;coffee&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;25&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;...e-of-a-kind vintange DHH coffee mug signed by the man hi...&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you need to change the &lt;code class=&quot;highlighter-rouge&quot;&gt;...&lt;/code&gt; into something else, use the &lt;code class=&quot;highlighter-rouge&quot;&gt;omission&lt;/code&gt; option.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;A one-of-a-kind vintange DHH coffee mug signed by the man himself!&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;coffee&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;...tange DHH coffee mug signe...&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;coffee&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;omission: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;---&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;---tange DHH coffee mug signe---&quot;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;excerpt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;coffee&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;radius: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;omission: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;snip&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;snip&amp;gt;tange DHH coffee mug signe&amp;lt;snip&amp;gt;&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-excerpt&quot;&gt;TextHelper#excerpt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActionText/RichText.html#method-i-to_plain_text&quot;&gt;ActionText#to_plain_text&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Use Rails `link_to_unless_current` for navigation links</title>
        <link href="https://boringrails.com/tips/rails-link-to-unless-current" rel="alternate" type="text/html" title="Use Rails `link_to_unless_current` for navigation links" />
        <published>2021-02-11T13:00:00+00:00</published>
        <updated>2021-02-11T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-link-to-unless-current</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-link-to-unless-current">&lt;p&gt;We’re all familiar with the classic Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;link_to&lt;/code&gt; helper. But did you know there is a &lt;code class=&quot;highlighter-rouge&quot;&gt;link_to_unless_current&lt;/code&gt; variant?&lt;/p&gt;

&lt;p&gt;It works just link &lt;code class=&quot;highlighter-rouge&quot;&gt;link_to&lt;/code&gt; except that it doesn’t create a link if the browsers current URL is the same as the link target.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/link_to_unless_current.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Simply replace &lt;code class=&quot;highlighter-rouge&quot;&gt;link_to&lt;/code&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;link_to_unless_current&lt;/code&gt;. If you are not already on the page, a normal &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag will be rendered. If you are on the page already, only the text will be rendered.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;nav&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col space-y-1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to_unless_current&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Dashboard&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dashboard_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to_unless_current&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;teams_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to_unless_current&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Projects&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;projects_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When you are on &lt;code class=&quot;highlighter-rouge&quot;&gt;/dashboard&lt;/code&gt;, this template will output:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;nav&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col space-y-1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  Dashboard
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/teams&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Teams&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/projects&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Projects&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also pass in a block to render (instead of the link name). This can be useful for making shared views. For instance, you could make a partial with actions for a &lt;code class=&quot;highlighter-rouge&quot;&gt;Post&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to_unless_current&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;+ Comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_comment_path&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Back&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts_path&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;other-variants&quot;&gt;Other Variants&lt;/h2&gt;

&lt;p&gt;As you might expect, there are other variants like &lt;code class=&quot;highlighter-rouge&quot;&gt;link_to_if&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;link_to_unless&lt;/code&gt; if you want to specify your own conditional logic for rendering a link or not.&lt;/p&gt;

&lt;p&gt;You might want to show a list of all &lt;code class=&quot;highlighter-rouge&quot;&gt;Post&lt;/code&gt;s, but if you are logged in as an admin, the title should be a link to an edit page.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to_if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;admin?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;manage_post_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to_unless_current&quot;&gt;UrlHelper#link_to_unless_current&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to_if&quot;&gt;UrlHelper#link_to_if&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to_unless&quot;&gt;UrlHelper#link_to_unless&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Use Rails `cycle` to avoid `i % 2 == 0` in your view loops</title>
        <link href="https://boringrails.com/tips/rails-cycle-for-view-loops" rel="alternate" type="text/html" title="Use Rails `cycle` to avoid `i % 2 == 0` in your view loops" />
        <published>2021-02-09T13:00:00+00:00</published>
        <updated>2021-02-09T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-cycle-for-view-loops</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-cycle-for-view-loops">&lt;p&gt;Sometimes you need to keep track of how many times you’ve looped when rendering some views, for instance to alternate background colors to create a “striped” table.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- @foods = [&quot;apple&quot;, &quot;orange&quot;, &quot;banana&quot;] --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@foods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;food&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;???&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;food&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You might try using &lt;code class=&quot;highlighter-rouge&quot;&gt;:odd&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;:even&lt;/code&gt; CSS child selectors or switching to &lt;code class=&quot;highlighter-rouge&quot;&gt;each_with_index&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@foods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;food&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bg-gray-200&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bg-gray-100&apos;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;food&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You could even refactor a bit to use &lt;code class=&quot;highlighter-rouge&quot;&gt;i.odd?&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;i.even?&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@foods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;food&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;even?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bg-gray-200&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bg-gray-100&apos;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;food&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Rails offers a different helper that comes in handy for these situations: &lt;code class=&quot;highlighter-rouge&quot;&gt;cycle&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;cycle&lt;/code&gt; helper takes an array of arguments and loops through them each time it is called.&lt;/p&gt;

&lt;p&gt;We could replace the above code with:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@foods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;food&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;bg-gray-200&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bg-gray-100&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;food&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The real benefits start to appear if you need more than two options.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@foods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;food&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;bg-red-100&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bg-orange-100&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;bg-yellow-100&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;food&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you need to manually reset or share a &lt;code class=&quot;highlighter-rouge&quot;&gt;cycle&lt;/code&gt; between code, you can pass a &lt;code class=&quot;highlighter-rouge&quot;&gt;name:&lt;/code&gt; key as an option.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;red&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;white&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;blue&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;colors&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;md&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;lg&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;xl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sizes&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;reset_cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;colors&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;reset_cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sizes&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also use any object that responds to &lt;code class=&quot;highlighter-rouge&quot;&gt;to_s&lt;/code&gt; in the cycle.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;rotate-&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cycle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;45&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;135&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;180&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-cycle&quot;&gt;TextHelper#cycle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tailwind Docs: &lt;a href=&quot;https://tailwindcss.com/docs/hover-focus-and-other-states#even-child&quot;&gt;Even/odd variants&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Use the Rails helper `highlight` when showing search results</title>
        <link href="https://boringrails.com/tips/rails-highlight-search-results" rel="alternate" type="text/html" title="Use the Rails helper `highlight` when showing search results" />
        <published>2021-02-08T13:00:00+00:00</published>
        <updated>2021-02-08T13:00:00+00:00</updated>
        <id>https://boringrails.com/tips/rails-use-highlight-for-search-results</id>
        
        
          <content type="html" xml:base="https://boringrails.com/tips/rails-highlight-search-results">&lt;p&gt;Use the Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;highlight&lt;/code&gt; helper to wrap search result matches in &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/highlight.png&quot; alt=&quot;Rails highlight helper&quot; /&gt;&lt;/p&gt;

&lt;p class=&quot;caption&quot;&gt;Highlight the search term “comment” in a list of notifcations&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;Pass the search term to your controller via params (e.g. &lt;code class=&quot;highlighter-rouge&quot;&gt;params[:search]&lt;/code&gt;) and use that to filter down your results.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/controllers/inbox_controller.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InboxController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;index&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@notifications&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;notifications&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;for_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# app/models/notification.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Notification&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:recipient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class_name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User&quot;&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;validates&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;presence: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;for_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;term&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;term&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present?&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Implement searching however you&apos;d like&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;message ILIKE ?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;term&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;%&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;all&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Render the notifications and use &lt;code class=&quot;highlighter-rouge&quot;&gt;TextHelper#highlight&lt;/code&gt; on the message to emphasis the matching query.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@notifications&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;notification&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;notification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can style the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; tag however you’d like.&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;mark&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;background-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;yellow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;options&quot;&gt;Options&lt;/h2&gt;

&lt;p&gt;You can pass either a string, array, or a regex as the phrase to match. Matches are case-insensitive.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Boring Rails is the best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;rails&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Boring &amp;lt;mark&amp;gt;Rails&amp;lt;/mark&amp;gt; is the best&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Boring Rails is the best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sr&quot;&gt;/rails|best/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Boring &amp;lt;mark&amp;gt;Rails&amp;lt;/mark&amp;gt; is the &amp;lt;mark&amp;gt;best&amp;lt;/mark&amp;gt;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Boring Rails is the best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;is&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Boring Rails &amp;lt;mark&amp;gt;is&amp;lt;/mark&amp;gt; the &amp;lt;mark&amp;gt;best&amp;lt;/mark&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If there are no matches or you leave the phrase blank, everything still works fine.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Boring Rails is the best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;JavaScript&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Boring Rails is the best&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Boring Rails is the best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# nil works too&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Boring Rails is the best&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also override the HTML markup wrapped around the matches using the &lt;code class=&quot;highlighter-rouge&quot;&gt;highlighter&lt;/code&gt; option. Use &lt;code class=&quot;highlighter-rouge&quot;&gt;\1&lt;/code&gt; to reference the match. The output is sanitized by default&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Boring is best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;highlighter: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&amp;lt;b&amp;gt;\1&amp;lt;/b&amp;gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Boring is &amp;lt;b&amp;gt;best&amp;lt;/b&amp;gt;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Boring is best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;best&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;highlighter: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&amp;lt;a href=&quot;tagged?q=\1&quot;&amp;gt;\1&amp;lt;/a&amp;gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Boring is &amp;lt;a href=\&quot;tagged?q=best\&quot;&amp;gt;best&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you need to run additional code, you can pass a block to render instead.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;highlight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Blog: Boring Rails&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;rails&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;public_share_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;term: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# =&amp;gt; Blog: Boring &amp;lt;a href=&quot;/public/share?term=Rails&quot;&amp;gt;Rails&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;p&gt;Rails API Docs: &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-highlight&quot;&gt;TextHelper#highlight&lt;/a&gt;&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
        

        
          <category term="ruby" />
        
          <category term="rails" />
        
          <category term="product" />
        

        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/tip-sticker.png" />
          <media:content medium="image" url="https://boringrails.com/images/tip-sticker.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Magic Responsive Tables with Stimulus and IntersectionObserver</title>
        <link href="https://boringrails.com/articles/responsive-tables-stimulus-intersection-observer/" rel="alternate" type="text/html" title="Magic Responsive Tables with Stimulus and IntersectionObserver" />
        <published>2021-01-13T13:00:00+00:00</published>
        <updated>2021-01-13T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/responsive-tables-stimulus-intersection-observer</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/responsive-tables-stimulus-intersection-observer/">&lt;p class=&quot;guest-intro&quot;&gt;This is a guest collaboration with &lt;a href=&quot;https://pascallaliberte.me/&quot;&gt;Pascal Laliberté&lt;/a&gt;, author of &lt;a href=&quot;https://modestjs.works/&quot;&gt;Modest JS Works&lt;/a&gt;, a short online book for those who want to write modest JavaScript, and then focus on all the other stuff that matters in building an app.&lt;/p&gt;

&lt;p&gt;You’re working on this data table for your app. It’s mostly server-side HTML. Nothing fancy.&lt;/p&gt;

&lt;p&gt;But as you’re adding columns, you’ve got a problem. How are you going to handle small screens?&lt;/p&gt;

&lt;p&gt;The table has to scroll horizontally to let the user see all the columns. The table needs to become “responsive”.&lt;/p&gt;

&lt;p&gt;In this article, we’ll look at a side-scrolling widget used in &lt;a href=&quot;https://polaris.shopify.com/&quot;&gt;Shopify’s Polaris UI toolkit&lt;/a&gt; (currently built in React), and we’ll recreate the functionality using just &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; without having to rewrite your data table in React.&lt;/p&gt;

&lt;p&gt;And instead of adding resize watchers and scroll watchers like the original React component uses, we’ll be using the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API&quot;&gt;IntersectionObserver API&lt;/a&gt;, a new browser feature that’s widely available.&lt;/p&gt;

&lt;div class=&quot;relative&quot; style=&quot;padding-top: 56.25%&quot;&gt;
  &lt;iframe class=&quot;absolute inset-0 w-full h-full&quot; src=&quot;https://player.vimeo.com/video/500278468&quot; frameborder=&quot;0&quot; allow=&quot;autoplay; fullscreen&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;h2 id=&quot;quick-intro-to-stimulus&quot;&gt;Quick Intro to Stimulus&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; a small library that helps you add sprinkles of progressive interactivity to your existing HTML.&lt;/p&gt;

&lt;p&gt;Just as CSS adds styling to elements as they appear in the Document Object Model (DOM), Stimulus adds interactivity (event handlers, actions, etc.) to elements as they appear in the DOM too (and removes it when they’re removed from the DOM). We’ll be using it here because it pairs so well with Rails, and with server-side rendered HTML.&lt;/p&gt;

&lt;p&gt;And just like you can tie styling by adding CSS classes to your HTML, you can tie interactivity by adding special Stimulus &lt;code class=&quot;highlighter-rouge&quot;&gt;data-&lt;/code&gt; attributes to elements. Stimulus watches for those, and when there’s a match, it fires up its interactivity (matching a Stimulus “controller” here named &lt;code class=&quot;highlighter-rouge&quot;&gt;table-scroll&lt;/code&gt;).&lt;/p&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-controller=&amp;quot;table-scroll&amp;quot;&quot; /&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-action=&amp;quot;table-scroll#scrollRight&amp;quot;&quot; /&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-table-scroll-target=&amp;quot;scrollRightButton&amp;quot;&quot; /&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;table-scroll&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;button button-scroll-right&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;scrollRightButton&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;table-scroll#scrollRight&quot;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;re-creating-the-scrolling-nav-from-shopify-polaris-data-tables&quot;&gt;Re-creating the Scrolling Nav from Shopify Polaris Data Tables&lt;/h2&gt;

&lt;p&gt;Shopify’s UI library introduces a clever side-scrolling navigation widget that is only shown when there are more columns than can fit in the context. There are Buttons to scroll left and right and small dots to show how many columns are in view.&lt;/p&gt;

&lt;p&gt;While the original is in React, we’ll recreate the functionality using Stimulus. The HTML here is from Shopify’s implementation: if you strip all of the Polaris classes, you’ll have the structure to style it to fit your own application’s styles.&lt;/p&gt;

&lt;p&gt;So let’s start by creating the overall markup structure you’ll be coding in your application and attaching the &lt;code class=&quot;highlighter-rouge&quot;&gt;table-scroll&lt;/code&gt; Stimulus controller.&lt;/p&gt;

&lt;p&gt;(Please note that some CSS styles have been omitted for brevity, I’ve tried to call out the critical classes where possible.)&lt;/p&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-table-scroll-target=&amp;quot;scrollArea&amp;quot;&quot; /&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;table-scroll&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;navBar&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Navigation widget --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col mx-auto&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;overflow-x-auto&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;scrollArea&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;table&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;min-w-full&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Table contents --&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next let’s set up the targets for each column by adding an attribute to the &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;th&amp;gt;&lt;/code&gt; tags. We can take advantage of Stimulus’ multiple target binding by setting all of the columns to a target value of &lt;code class=&quot;highlighter-rouge&quot;&gt;column&lt;/code&gt;, which will allow us to automatically bind a &lt;code class=&quot;highlighter-rouge&quot;&gt;columnTargets&lt;/code&gt; array in our Stimulus controller.&lt;/p&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-table-scroll-target=&amp;quot;column&amp;quot;&quot; /&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Table contents --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;table&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;min-w-full&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;thead&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;tr&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Product&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Price&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;SKU&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Sold&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;th&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Net Sales&lt;span class=&quot;nt&quot;&gt;&amp;lt;/th&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/thead&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Table body --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/table&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next let’s build the markup for the navigation widget. We will use a dot icon for each of the columns and a left and right arrow to scroll the table.&lt;/p&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-table-scroll-target=&amp;quot;leftButton&amp;quot; data-action=&amp;quot;table-scroll#scrollLeft&amp;quot;&quot; /&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-table-scroll-target=&amp;quot;columnVisibilityIndicator&amp;quot;&quot; /&gt;

&lt;meta data-controller=&quot;callout&quot; data-callout-text-value=&quot;data-table-scroll-target=&amp;quot;rightButton&amp;quot; data-action=&amp;quot;table-scroll#scrollRight&amp;quot;&quot; /&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Navigation widget --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;navBar&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Left button --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;leftButton&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;table-scroll#scrollLeft&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;svg&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Column visibility dots --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;times&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-gray-200&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;columnVisibilityIndicator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;svg&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Scroll Right button --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-table-scroll-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;rightButton&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;table-scroll#scrollRight&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;svg&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And lastly, let’s pass in some class data to define CSS styles to apply when the navigation widget should be shown or hidden and how the buttons and dots should be styled. You may choose to hard-code these classes into the Stimulus controller, but you may want to make them configurable depending on the needs of your project (for example, you may want to use this controller with multiple tables but use a different color to indicate the visible columns).&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;table-scroll&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-table-scroll-nav-shown-class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-table-scroll-nav-hidden-class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-table-scroll-button-disabled-class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-gray-200&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-table-scroll-indicator-visible-class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-blue-600&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- The rest of the markup --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;using-intersectionobserver-to-bring-it-to-life&quot;&gt;Using IntersectionObserver to bring it to life&lt;/h2&gt;

&lt;p&gt;Now that we’ve annotated the markup, we can add the Stimulus controller.&lt;/p&gt;

&lt;p&gt;We’ll need some way of watching the &lt;code class=&quot;highlighter-rouge&quot;&gt;scrollArea&lt;/code&gt; position and detecting what is visible. Unlike the Polaris implementation, we’ll use the &lt;code class=&quot;highlighter-rouge&quot;&gt;IntersectionObserver&lt;/code&gt; API. No need for &lt;code class=&quot;highlighter-rouge&quot;&gt;window.resize&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;window.scroll&lt;/code&gt;, which are more costly on performance than the new native &lt;code class=&quot;highlighter-rouge&quot;&gt;IntersectionObserver&lt;/code&gt; browser API.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;IntersectionObserver&lt;/code&gt; API watches the visibility of elements, and a callback is fired on visibility changes. In our case, we’ll be watching the visibility of column headings.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// controllers/table_scroll_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;navBar&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;scrollArea&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;leftButton&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;rightButton&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;columnVisibilityIndicator&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;classes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;navShown&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;navHidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;buttonDisabled&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;indicatorVisible&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// start watching the scrollAreaTarget via IntersectionObserver&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// stop watching the scrollAreaTarget, teardown event handlers&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Since we’re progressively enhancing the page with Stimulus, we should take care to check if the browser supports &lt;code class=&quot;highlighter-rouge&quot;&gt;IntersectionObserver&lt;/code&gt; and degrade gracefully if not.&lt;/p&gt;

&lt;p&gt;When the controller is connected, we create an &lt;code class=&quot;highlighter-rouge&quot;&gt;IntersectionObserver&lt;/code&gt; and provide a callback and then register that we want to observe all of our &lt;code class=&quot;highlighter-rouge&quot;&gt;columnTargets&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each time the &lt;code class=&quot;highlighter-rouge&quot;&gt;updateScrollNavigation&lt;/code&gt; callback is fired, (which also fires by default when intersectionObserver is initialized), we’ll update each column heading’s &lt;code class=&quot;highlighter-rouge&quot;&gt;data-is-visible&lt;/code&gt; attribute, to be checked later by the other callbacks.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;supportsIntersectionObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;IntersectionObserver&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;IntersectionObserverEntry&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt;
    &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;intersectionRatio&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;IntersectionObserverEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;prototype&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;classes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;startObservingColumnVisibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;startObservingColumnVisibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;supportsIntersectionObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`This browser doesn&apos;t support IntersectionObserver`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intersectionObserver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;IntersectionObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateScrollNavigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scrollAreaTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;threshold&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.99&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// otherwise, the right-most column sometimes won&apos;t be considered visible in some browsers, rounding errors, etc.&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;headingEl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intersectionObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;headingEl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;updateScrollNavigation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;observerRecords&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;observerRecords&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isIntersecting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggleScrollNavigationVisibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateColumnVisibilityIndicators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateLeftRightButtonAffordance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stopObservingColumnVisibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;stopObservingColumnVisibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intersectionObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intersectionObserver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There is a bit of code to set up and register things, but it’s fairly straightforward and from here, the remaining work is to sync the visibility of the columns with the navigation widget.&lt;/p&gt;

&lt;p&gt;You can see that we use the target binding in Stimulus to toggle on and off CSS classes in the page. And since we made the CSS class configurable, you can tweak the UI by editing the HTML, not rebuilding your JavaScript bundle.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;toggleScrollNavigationVisibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;allColumnsVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt;
      &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;allColumnsVisible&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navBarTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navShownClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navBarTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navHiddenClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navBarTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navShownClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navBarTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;navHiddenClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;updateColumnVisibilityIndicators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;headingEl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;indicator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnVisibilityIndicatorTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;indicator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;indicator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;indicatorVisibleClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;headingEl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;updateLeftRightButtonAffordance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;firstColumnHeading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lastColumnHeading&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateButtonAffordance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;leftButtonTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;firstColumnHeading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;updateButtonAffordance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;rightButtonTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;lastColumnHeading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;updateButtonAffordance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;isDisabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isDisabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setAttribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buttonDisabledClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;removeAttribute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;button&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;buttonDisabledClass&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lastly, we need to add the actions that are triggered when clicking the navigation buttons. When the buttons are clicked, we find the next non-visible column in the scrolling direction and then scroll the table so the leading edge of the column.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;scrollLeft&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// scroll to make visible the first non-fully-visible column to the left of the scroll area&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scrollAreaTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;offsetLeft&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;scrollRight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// scroll to make visible the first non-fully-visible column to the right of the scroll area&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// right to left&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;isVisible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scrollAreaTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scroll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;columnToScrollTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;offsetLeft&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can view the full code &lt;a href=&quot;https://gist.github.com/swanson/722f890c7fb495443af3f699f25e30e5&quot;&gt;via this gist&lt;/a&gt; or play with an interactive example &lt;a href=&quot;https://codepen.io/pascallaliberte/pen/MWjvGaj?editors=1011&quot;&gt;via this Codepen&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&gt;Wrap it up&lt;/h2&gt;

&lt;p&gt;And voila! We’ve got a pretty nifty responsive scrolling table. On large screens, it looks like a normal HTML table. But as you shrink the view port, the navigation widget appears and you can see the dots helping to show what part of the table is visible.&lt;/p&gt;

&lt;p&gt;Overall, this controller comes in at under 200 lines of code and should be able to handle various sized tables throughout your app.&lt;/p&gt;

&lt;p&gt;With the release of &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire&lt;/a&gt;, Stimulus is an important piece for the “last mile” of interactivity in non-SPA apps. While Stimulus is often used for running small bits of JavaScript, you can build out more robust controllers that mirror fully featured UI libraries.&lt;/p&gt;

&lt;p&gt;Before you completely change your application architecture to use a fancy client-side framework, see if you can get by with your existing HTML markup and a bit of Stimulus.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Responsive HTML data tables are a tricky problem that usually requires scrolling on small screens. With a sprinkle of Stimulus and the IntersectionObserver API, we can build a small enhancement to make the user experience more pleasant.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/table-wizard.png" />
          <media:content medium="image" url="https://boringrails.com/images/table-wizard.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Hacktoberfest Recap: Open source Ruby/Rails work in 2020</title>
        <link href="https://boringrails.com/articles/hacktoberfest-2020-recap/" rel="alternate" type="text/html" title="Hacktoberfest Recap: Open source Ruby/Rails work in 2020" />
        <published>2020-11-16T13:00:00+00:00</published>
        <updated>2020-11-16T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/hacktoberfest-2020-recap</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/hacktoberfest-2020-recap/">&lt;p&gt;&lt;a href=&quot;https://hacktoberfest.digitalocean.com/&quot;&gt;Hacktoberfest&lt;/a&gt; is a month long event that encourages developers to contribute to open source – submit four valid pull requests and you win a tshirt. Despite the &lt;a href=&quot;https://joel.net/how-one-guy-ruined-hacktoberfest2020-drama&quot;&gt;criticisms and controversy&lt;/a&gt; this year, I’m a fan of the event and it did help motivate me to make some contributions.&lt;/p&gt;

&lt;p&gt;I wanted to share a recap of my submissions for Hacktoberfest because I think it’s a good sampling of what options are available to folks looking to contribute to Ruby and Rails. There are lots of ways to give back outside of the main &lt;code class=&quot;highlighter-rouge&quot;&gt;rails/rails&lt;/code&gt; repository!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/hacktoberfest-2020.png&quot; alt=&quot;My 2020 Hacktoberfest Contributions&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;viewcomponent&quot;&gt;ViewComponent&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href=&quot;https://github.com/github/view_component/pull/493&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;github/view_component&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pull request: &lt;a href=&quot;https://github.com/github/view_component/pull/493&quot;&gt;Allow preview controller to be customized via config options&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;view_component&lt;/code&gt; gem helps you organize your server-rended view partials into “components” that are backed by a Ruby class. If you’re familiar with the built-in Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;partials&lt;/code&gt; and gems that implement decorator or presenter patterns (like &lt;code class=&quot;highlighter-rouge&quot;&gt;draper&lt;/code&gt;), you can think of &lt;code class=&quot;highlighter-rouge&quot;&gt;view_component&lt;/code&gt; as a mash-up of those two approaches.&lt;/p&gt;

&lt;p&gt;I’ve been playing around with this gem on my own projects and think it’s heading in a great direction. I found an existing &lt;a href=&quot;https://github.com/github/view_component/issues/434&quot;&gt;issue&lt;/a&gt; with a “help-wanted” tag and decided to see if I could add the feature.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;view_component&lt;/code&gt; gem allows you to test out your components in a sandbox mode (akin to Rail’s &lt;code class=&quot;highlighter-rouge&quot;&gt;mailer&lt;/code&gt; previews). There was a feature request to support customizing the controller used to render these previews in case you want to add authorization or other special application-specific things.&lt;/p&gt;

&lt;p&gt;The pull request was merged and release in &lt;a href=&quot;https://github.com/github/view_component/blob/master/CHANGELOG.md#2200&quot;&gt;version 2.20.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gems like this are a great starting point for contributing. You certainly have more experience using a gem in your own application than working deep in the Rails internals so adding features or fixing bugs in a gem will be more approachable.&lt;/p&gt;

&lt;p&gt;It was extra helpful that the maintainers tagged issues with “help-wanted” and there was low enough “traffic” that I was able to get questions answered and my code reviewed very quickly.&lt;/p&gt;

&lt;h2 id=&quot;bullet&quot;&gt;Bullet&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href=&quot;https://github.com/flyerhzm/bullet&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;flyerhzm/bullet&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pull request: &lt;a href=&quot;https://github.com/flyerhzm/bullet/pull/522&quot;&gt;Update design for footer notification&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;bullet&lt;/code&gt; gem is one of my go-to tools when building Rails apps. This gem will flag N+1 queries as you’re developing a feature and alert you to associations you should preload. It’s a lot nicer to find and stamp out these performance problems as you go instead of months later when they appear in production.&lt;/p&gt;

&lt;p&gt;You can configure how &lt;code class=&quot;highlighter-rouge&quot;&gt;bullet&lt;/code&gt; notifies you of problems. My default preference is a browser popup – it’s annoying but it does drive me to fix the problem instead of ignoring it. But sometimes there are false positives or I’m prototyping a feature and it becomes too much. So I went to switch the configuration to use the “footer” where it would add a banner to the page with the error message.&lt;/p&gt;

&lt;p&gt;But I really disliked the design: it took of a ton of space and was, well, ugly.&lt;/p&gt;

&lt;p&gt;So I decided to change it. You can see the “Before” and “After” &lt;a href=&quot;https://github.com/flyerhzm/bullet/pull/522#issue-495898756&quot;&gt;here&lt;/a&gt;. The new design is expandable and gets out of the way.&lt;/p&gt;

&lt;p&gt;The pull request was merged and will be included in the next release of the gem.&lt;/p&gt;

&lt;h2 id=&quot;lrug&quot;&gt;LRUG&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href=&quot;https://github.com/lrug/lrug.org&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;lrug/lrug.org&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pull request: &lt;a href=&quot;https://github.com/lrug/lrug.org/pull/130&quot;&gt;Add link to StimulusJS talk slides&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://lrug.org/&quot;&gt;LRUG&lt;/a&gt; is a local Ruby meetup group in London. While this contribution on GitHub was tiny (merely updating their website with a link to the slides for a talk I gave), it’s still an example of how to contribute to the Ruby community.&lt;/p&gt;

&lt;p&gt;Meetups are always looking for volunteer speakers and given that many groups are doing virtual meetups in 2020, I thought it might be fun to branch out from my local meetup (Indianapolis) and offer to speak at two groups in Europe. I gave a talk about using StimulusJS to the &lt;a href=&quot;https://www.rug-b.de/events/ruby-usergroup-berlin-september-2020-637&quot;&gt;Berlin Ruby user group&lt;/a&gt; and the &lt;a href=&quot;https://lrug.org/meetings/2020/september/&quot;&gt;London group&lt;/a&gt;. It was cool to be an “international speaker” and I was able to take advantage of the timezone difference to simply slot the presentation into my normal workday calendar (lunchtime in the US is evening time in the EU).&lt;/p&gt;

&lt;p&gt;Overall I had a great experience and a warm welcome from my new Ruby friends on the otherside of the world.&lt;/p&gt;

&lt;h2 id=&quot;ruby-for-good-circulate&quot;&gt;Ruby For Good: Circulate&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href=&quot;https://github.com/rubyforgood/circulate&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;rubyforgood/circulate&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pull request: &lt;a href=&quot;https://github.com/rubyforgood/circulate/pull/257&quot;&gt;Add power source field to items&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I came across the &lt;a href=&quot;https://rubyforgood.org/&quot;&gt;Ruby For Good organization&lt;/a&gt; when looking for projects to contribute to and found that they manage a number of &lt;a href=&quot;https://github.com/rubyforgood&quot;&gt;Ruby and Rails projects&lt;/a&gt; that help non-profits that need software development work done. There are apps for helping with conservation, diaper banks, volunteer management, and more.&lt;/p&gt;

&lt;p&gt;I found a project that is building an inventory management system for tool-lending libraries. The idea is that you can borrow tools from a community pool or schedule time to use machinery that is usually too expensive for an individual to own themselves.&lt;/p&gt;

&lt;p&gt;The Ruby for Good projects seem to all have a team of people to help triage and define work items and there is a Slack channel to coordinate and ask questions. I thought it was really neat to be able to contribute to an open source project where you were writing features like most developers do as part of their “normal” job. Writing frameworks and libraries is critical, but it takes a different set of skills and mindset than doing application development.&lt;/p&gt;

&lt;p&gt;I was able to get the project up and running locally in under an hour and I banged out a feature to add a new field to &lt;code class=&quot;highlighter-rouge&quot;&gt;Item&lt;/code&gt;s in the system to record what kind of power source the tool uses.&lt;/p&gt;

&lt;p&gt;The pull request was merged and deployed to the &lt;a href=&quot;https://app.chicagotoollibrary.org/&quot;&gt;Chicago Tool Library&lt;/a&gt; app.&lt;/p&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&gt;Wrap it up&lt;/h2&gt;

&lt;p&gt;Despite the flaws and issues with incentives, I find Hacktoberfest to personally be a very motivating effort. There are several ways to contribute to the Ruby or Rails open source ecosystems – even if you aren’t keen to dive into a huge framework or project.&lt;/p&gt;

&lt;p&gt;You can start by looking at some of your favorite gems and see if there are improvements you can make – after all, your perspective as an end-user is invaluable.&lt;/p&gt;

&lt;p&gt;Consider giving a talk at a local (or remote!) user group. No matter how far along in your programming journey you are, you can always share something about what you’ve learned that can help someone else.&lt;/p&gt;

&lt;p&gt;And lastly, consider checking our Ruby for Good if you’re looking to practice more of the day-to-day Rails development skills. These projects seem like a great environment for junior developers as they have dedicated folks involved to help review code and write feature specifications.&lt;/p&gt;

&lt;p&gt;Did you complete Hacktoberfest? If so, I’d love to hear about your contributions on &lt;a href=&quot;https://twitter.com/_swanson&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">A recap of my Rails-related contributions for the 2020 Hacktoberfest event: ViewComponent, Bullet, LRUG, and Circulate</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/hacktoberfest.png" />
          <media:content medium="image" url="https://boringrails.com/images/hacktoberfest.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Building GitHub-style Hovercards with StimulusJS and HTML-over-the-wire</title>
        <link href="https://boringrails.com/articles/hovercards-stimulus/" rel="alternate" type="text/html" title="Building GitHub-style Hovercards with StimulusJS and HTML-over-the-wire" />
        <published>2020-06-22T13:00:00+00:00</published>
        <updated>2020-06-22T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/hovercards-stimulus</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/hovercards-stimulus/">&lt;p&gt;Somewhere along the way toward our current JavaScript hellscape, programmers decided that HTML was over. We’re done with it.&lt;/p&gt;

&lt;p&gt;The emergence of tools like &lt;a href=&quot;https://reactjs.org/docs/hello-world.html&quot;&gt;React&lt;/a&gt; shifted programmers away from writing HTML, instead writing &lt;a href=&quot;https://reactjs.org/docs/introducing-jsx.html&quot;&gt;JSX&lt;/a&gt;, a fancier tag-based markup language that worked nicely inside your JavaScript.&lt;/p&gt;

&lt;p&gt;Backends were then relegated to being dumb JSON API endpoints. Or if you were fancy and chasing upvotes, you’d use &lt;a href=&quot;https://graphql.org/&quot;&gt;GraphQL&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;But HTML? Yuck!&lt;/p&gt;

&lt;h2 id=&quot;a-brief-history-of-html-over-the-wire&quot;&gt;A Brief History of HTML-over-the-wire&lt;/h2&gt;

&lt;p&gt;One of the key pillars of Rails is to &lt;a href=&quot;https://rubyonrails.org/doctrine/#integrated-systems&quot;&gt;“Value integrated systems”&lt;/a&gt;. While the industry moves towards microservices, highly decoupled front-ends and teams, and the siren song of Programming via LEGO Bricks, Rails leans into one system that does it all – termed the &lt;a href=&quot;https://m.signalvnoise.com/the-majestic-monolith/&quot;&gt;Majestic Monolith&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of rebuilding much of what already works in Rails in a client-side JavaScript MVC framework, apps like Basecamp, GitHub, and Shopify are able to achieve snappy page loads using the concept of “HTML-over-the-wire”.&lt;/p&gt;

&lt;p&gt;In his &lt;a href=&quot;https://www.youtube.com/watch?v=SWEts0rlezA&quot;&gt;seminal RailsConf 2016 talk&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/sstephenson&quot;&gt;Sam Stephenson&lt;/a&gt; walks through the pieces of this stack.&lt;/p&gt;

&lt;p&gt;By using &lt;a href=&quot;https://github.com/turbolinks/turbolinks&quot;&gt;Turbolinks&lt;/a&gt; (or similar libraries like &lt;a href=&quot;https://github.com/defunkt/jquery-pjax&quot;&gt;pjax&lt;/a&gt; or &lt;a href=&quot;https://inertiajs.com/how-it-works&quot;&gt;Inertia&lt;/a&gt;) and fast HTML responses (aided by caching and avoiding excessive database queries to get &lt;a href=&quot;https://www.youtube.com/watch?v=eBccDerJPJE&quot;&gt;sub-100ms response times&lt;/a&gt;), you could build high performance pages, while still hanging on to the understated benefits of stateless HTTP responses and server-side logic.&lt;/p&gt;

&lt;p&gt;As Sam points out, it was truly a “Golden Age of Web Development”.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/golden-age.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So while much of the industry went down the JavaScript rabbit hole – creating new innovations for &lt;a href=&quot;https://reactjs.org/docs/reconciliation.html&quot;&gt;reactive rendering&lt;/a&gt;, &lt;a href=&quot;https://github.com/reduxjs/redux-thunk&quot;&gt;functional&lt;/a&gt; &lt;a href=&quot;https://redux.js.org/&quot;&gt;state management containers&lt;/a&gt;, and &lt;a href=&quot;https://reach.tech/router&quot;&gt;approximately&lt;/a&gt; &lt;a href=&quot;https://github.com/ReactTraining/react-router&quot;&gt;seventy&lt;/a&gt; &lt;a href=&quot;https://github.com/frontarm/navi&quot;&gt;different&lt;/a&gt; &lt;a href=&quot;https://blog.remix.run/p/remix-preview&quot;&gt;client-side&lt;/a&gt; &lt;a href=&quot;https://redwoodjs.com/docs/redwood-router&quot;&gt;routing&lt;/a&gt; &lt;a href=&quot;https://github.com/4Catalyzer/found&quot;&gt;libraries&lt;/a&gt; – the quiet rebellion in Rails-land was honing these techniques and plugging along building apps out of boring server-rendered HTML.&lt;/p&gt;

&lt;p&gt;We’re seeing a renaissance of these tools in 2020 and the excitement (at least in a small corner of the Twitter!) is reaching a fever pitch as Basecamp launches HEY: a fully-featured email client with a tiny JavaScript footprint that pushed the boundaries of the HTML-over-the-wire approach.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/hey-js-tweet.png&quot; alt=&quot;Tweet about HEY Javascript&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;turbolinks--stimulus-20xx-the-future&quot;&gt;Turbolinks / Stimulus 20XX: The Future&lt;/h2&gt;

&lt;p&gt;The stack in 2014-2016 was:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Turbolinks/pjax&lt;/li&gt;
  &lt;li&gt;Rails UJS + &lt;code class=&quot;highlighter-rouge&quot;&gt;js.erb&lt;/code&gt; templates (&lt;a href=&quot;https://signalvnoise.com/posts/3697-server-generated-javascript-responses&quot;&gt;Server-generated JavaScript Responses&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Heavy HTML fragment caching&lt;/li&gt;
  &lt;li&gt;Rails Asset Pipeline and &lt;a href=&quot;https://coffeescript.org/&quot;&gt;CoffeeScript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can even trace the origin of these techniques back even further. I was recently &lt;a href=&quot;https://twitter.com/chris_vannoy/status/1274682764948844545&quot;&gt;sent a link&lt;/a&gt; to a nearly 15 year old REST “microformat” called &lt;a href=&quot;http://microformats.org/wiki/rest/ahah&quot;&gt;“AHAH: Asynchronous HTML and HTTP”&lt;/a&gt;, which is an early version of the same ideas we’re so excited about today. (You shouldn’t be surprised to see &lt;a href=&quot;https://twitter.com/dhh&quot;&gt;David Hansson&lt;/a&gt; listed as a contributor!)&lt;/p&gt;

&lt;p&gt;Now a “state-of-the-art” 2020 version also includes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;StimulusJS&lt;/a&gt; (see also &lt;a href=&quot;https://github.com/alpinejs/alpine&quot;&gt;AlpineJS&lt;/a&gt;) for lightweight event management, data binding, and “sprinkles” of behavior&lt;/li&gt;
  &lt;li&gt;Partial updates with Turbolinks via a new &lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt; command approach (replacing &lt;code class=&quot;highlighter-rouge&quot;&gt;js.erb&lt;/code&gt; and supporting &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP&quot;&gt;CSP&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;Real-time Turbolinks updates via &lt;a href=&quot;https://guides.rubyonrails.org/action_cable_overview.html&quot;&gt;ActionCable&lt;/a&gt; (see also &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;StimulusReflex&lt;/a&gt;/&lt;a href=&quot;https://cableready.stimulusreflex.com/&quot;&gt;CableReady&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;First-party support for Webpack, ES6, and new CSS approaches like &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind&lt;/a&gt; and &lt;a href=&quot;https://purgecss.com/&quot;&gt;PurgeCSS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This stack is extremely powerful and the development experience allows you to really fly. You can build fast and interactive applications with a small team, all while still experiencing the joy of a 2014-era vanilla Rails codebase.&lt;/p&gt;

&lt;p&gt;But years of a &lt;a href=&quot;https://macwright.org/2020/05/10/spa-fatigue.html&quot;&gt;JavaScript SPA-heavy monoculture&lt;/a&gt; have made it hard to learn about this stack. The community is filled with practitioners, using the tools to build software and businesses. There simply has not been the same level of content produced and so many of these tools are unknown and can be unapproachable.&lt;/p&gt;

&lt;p&gt;One of the ways I can contribute is to light the way for those want to know more by showing some real-world examples (not a &lt;a href=&quot;http://todomvc.com/&quot;&gt;TODO list&lt;/a&gt; or a &lt;a href=&quot;https://wsvincent.com/react-counter/&quot;&gt;Counter&lt;/a&gt;). Once you see how you can use tools like Stimulus and HTML responses to build features where you might instead reach for a tool like React, things will start to click.&lt;/p&gt;

&lt;h2 id=&quot;lets-build-something-real-hovercards&quot;&gt;Let’s Build Something Real: Hovercards&lt;/h2&gt;

&lt;p&gt;Hovercards show extra contextual information in a popup bubble when you hover over something in your app. You can see examples of this UI pattern on GitHub, Twitter, and even Wikipedia.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/hovercard-examples.png&quot; alt=&quot;Examples of hovercard UI&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This feature is really easy to build with Rails using an HTML-over-the-wire approach.&lt;/p&gt;

&lt;p&gt;Here’s the plan:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Build a controller action to render the hovercard as HTML&lt;/li&gt;
  &lt;li&gt;Write a tiny Stimulus controller to fetch the hovercard HTML when you hover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…and that’s it.&lt;/p&gt;

&lt;p&gt;We don’t need to make API endpoints and figure out how to structure all of the data we need. We don’t need to reach for React or Vue to make this a client-side component.&lt;/p&gt;

&lt;p&gt;The beauty of this boring Rails approach is that the feature is dead-simple and it’s equally straightforward to build. It’s easy to reason about the code and super extensible.&lt;/p&gt;

&lt;p&gt;For this example, let’s build the event feed for a sneaker marketplace app.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/shoe-hovercard.gif&quot; alt=&quot;Shoe feed hovercard example&quot; /&gt;&lt;/p&gt;

&lt;p&gt;When you hover over a shoe, you see a picture, the name, the price, etc. Same for the user, you can see a mini-profile for each user.&lt;/p&gt;

&lt;h3 id=&quot;the-frontend-stimulus--fetch&quot;&gt;The Frontend (Stimulus + fetch)&lt;/h3&gt;

&lt;p&gt;The markup for the link looks like:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/shoes/feed.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;inline-block&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hovercard&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-hovercard-url-value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hovercard_shoe_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mouseenter-&amp;gt;hovercard#show mouseleave-&amp;gt;hovercard#hide&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;branded-link&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p class=&quot;pro-tip&quot;&gt;Note: we are using the APIs from the &lt;a href=&quot;https://github.com/stimulusjs/stimulus/pull/202&quot;&gt;Stimulus 2.0&lt;/a&gt; preview release!&lt;/p&gt;

&lt;p&gt;One of the great features of Stimulus is that you can read the markup and understand what’s happening without diving into the JavaScript.&lt;/p&gt;

&lt;p&gt;Without knowing anything else about the implementation, you could guess how it’s going to work: this link is wrapped in a &lt;code class=&quot;highlighter-rouge&quot;&gt;hovercard&lt;/code&gt; controller, when you hover (via &lt;code class=&quot;highlighter-rouge&quot;&gt;mouseenter&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;mouseleave&lt;/code&gt; events) the card is shown or hidden.&lt;/p&gt;

&lt;p&gt;As recommended in &lt;a href=&quot;https://boringrails.com/articles/better-stimulus-controllers/&quot;&gt;Writing Better Stimulus Controllers&lt;/a&gt;, you should pass in the URL for the hover card endpoint as a data property so that we can re-use the &lt;code class=&quot;highlighter-rouge&quot;&gt;hovercard_controller&lt;/code&gt; for multiple types of cards. This also keeps us from having to duplicate the application routes in JavaScript.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/hovercard_controller.js&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;card&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hasCardTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cardTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;urlValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fragment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createRange&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createContextualFragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

          &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;hide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hasCardTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cardTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hasCardTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cardTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is all of the JavaScript we’re going to be writing for this feature: it’s only ~30 lines and we can use this for any other hovercards in the app. There isn’t really anything app specific about this controller either, you could pull it into a separate module and re-use it across projects. It’s totally generic.&lt;/p&gt;

&lt;p&gt;The controller uses the &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch&lt;/code&gt; API to call the provided Rails endpoint, gets some HTML back, and then inserts it into the DOM. As a small improvement, we use the Stimulus &lt;code class=&quot;highlighter-rouge&quot;&gt;target&lt;/code&gt; API for data binding to save a reference to the card so that subsequent hovers over this link can simply show/hide the markup without making another network request.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/hovercard-network.gif&quot; alt=&quot;Hovercard Network tab&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We also choose to remove the card when leaving the page (via the &lt;code class=&quot;highlighter-rouge&quot;&gt;disconnect&lt;/code&gt; lifecycle method), but you could also opt to hide the card instead depending on how you want caching to work.&lt;/p&gt;

&lt;h3 id=&quot;the-backend-rails--server-rendered-html&quot;&gt;The Backend (Rails + Server rendered HTML)&lt;/h3&gt;

&lt;p&gt;There is nothing magic on the frontend and it’s the same story on the backend.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/routes.rb&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;routes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;draw&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:shoes&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;member&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:hovercard&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Setup a route for &lt;code class=&quot;highlighter-rouge&quot;&gt;/shoes/:id/hovercard&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/controllers/shoes_controller.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ShoesController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hovercard&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@shoe&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;layout: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Write a basic controller action, the only difference being that we set &lt;code class=&quot;highlighter-rouge&quot;&gt;layout: false&lt;/code&gt; so that we do not use the global application layout for this endpoint.&lt;/p&gt;

&lt;p&gt;You can even visit this path directly in your browser to quickly iterate on the content and design. The workflow gets even better when using a utility-based styling approach like Tailwind since you don’t even need to wait for your asset bundles to rebuild!&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/shoes/hovercard.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;relative&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-hovercard-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;card&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-tooltip-arrow&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex space-x-3 items-center w-64&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;image_tag&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;image_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;flex-shrink-0 h-24 w-24 object-cover border border-gray-200 bg-gray-100 rounded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;alt: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-sm leading-5 font-medium text-indigo-600&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;brand&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;

        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-lg leading-0 font-semibold text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;

        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex text-sm text-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;colorway&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mx-1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;ni&quot;&gt;&amp;amp;middot;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;number_to_currency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The hovercard is built with a server-rended ERB template, same as any other page in the Rails app. We set the &lt;code class=&quot;highlighter-rouge&quot;&gt;data-hovercard-target&lt;/code&gt; as a convenience to bind to this element back in the Stimulus controller.&lt;/p&gt;

&lt;h3 id=&quot;finishing-touches&quot;&gt;Finishing Touches&lt;/h3&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;data-tooltip-arrow&lt;/code&gt; allows us to add a little triangle to the bubble with a bit of CSS. You can add a library like &lt;a href=&quot;https://popper.js.org/&quot;&gt;Popper&lt;/a&gt; if you have more advanced needs, but this single CSS rule works great and doesn’t require any external dependencies.&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;/* app/javascript/stylesheets/application.css */&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;data-tooltip-arrow&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;::after&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot; &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;absolute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;100%&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1rem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2rem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;white&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;transparent&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;transparent&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;transparent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And voila! We’ve built hovercards!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/shoe-closeup.gif&quot; alt=&quot;Hovercard for Shoes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;If we want to add a hovercard to another model type in our application (like User profiles), it almost feels like cheating. We can use the same Stimulus controller. All we need to do is add User specific template.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/users/hovercard.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;relative&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-hovercard-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;card&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-tooltip-arrow&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex space-x-3 items-center p-1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;image_tag&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;gravatar_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;flex-shrink-0 h-16 w-16 object-cover bg-gray-100 rounded inset shadow-inner&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;alt: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex-1 flex flex-col&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;font-bold text-lg&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex space-x-1 items-center text-sm&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;svg&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-orange-400 fill-current h-4 w-4&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;viewBox=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0 0 20 20&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-gray-500 italic&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bio&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-gray-400 text-xs mt-1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Kickin&apos; it since &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;year&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/images/user-closeup.gif&quot; alt=&quot;Hovercard for Users&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;taking-it-to-the-next-level&quot;&gt;Taking it to the next level&lt;/h2&gt;

&lt;p&gt;If you want to expand this feature even further, there are a few ideas you might consider:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Removing some duplication in the hovercard templates by either: extracting a Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;partial&lt;/code&gt;, using a gem like &lt;a href=&quot;https://github.com/github/view_component&quot;&gt;github/view_component&lt;/a&gt;, or using the Tailwind &lt;code class=&quot;highlighter-rouge&quot;&gt;@apply&lt;/code&gt; directive to &lt;a href=&quot;https://tailwindcss.com/docs/extracting-components/&quot;&gt;create components&lt;/a&gt; in your stylesheets&lt;/li&gt;
  &lt;li&gt;Animating the hovercard using &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions&quot;&gt;CSS transitions&lt;/a&gt; to fade in and out&lt;/li&gt;
  &lt;li&gt;Adding a delay or fancy “directional aiming” (like the &lt;a href=&quot;https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown&quot;&gt;Amazon mega dropdown&lt;/a&gt;) so that you can move your mouse more easily to the hovercard&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Aborting_a_fetch&quot;&gt;Cancel a pending AJAX request&lt;/a&gt; if you move away using the &lt;code class=&quot;highlighter-rouge&quot;&gt;AbortController&lt;/code&gt; for the &lt;code class=&quot;highlighter-rouge&quot;&gt;fetch&lt;/code&gt; API&lt;/li&gt;
  &lt;li&gt;Explore caching the hovercards (assuming the data is not specific to a user or session) in Rails with &lt;a href=&quot;https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching&quot;&gt;Fragment Caching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&gt;Wrap it up&lt;/h2&gt;

&lt;p&gt;This stack is a love letter to the web. Use links and forms. Render HTML. Keep your state on the server and in the database. Let the browser handle navigation. Add sprinkles of interactivity to improve the experience. For many it feels like a step backward, but in my opinion it’s going back to the way things should be.&lt;/p&gt;

&lt;p&gt;It’s natural to be skeptical, especially in the current climate of “JS all the things”. But you really have to give these tools a try before you really get it. Once you see that the classic ways of building software can still get the job done, it’s hard to go back to debugging &lt;code class=&quot;highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; conflicts or rebuilding HTML forms inside of this years framework du jour.&lt;/p&gt;

&lt;p&gt;In this &lt;a href=&quot;https://railsconf.com/2020/video/david-heinemeier-hansson-keynote-interview-with-david-heinemeier-hansson&quot;&gt;year’s RailsConf remote keynote&lt;/a&gt;, DHH talked about the &lt;a href=&quot;https://en.wikipedia.org/wiki/Thesis,_antithesis,_synthesis&quot;&gt;cyclical pendulum of Hegel’s dialectics&lt;/a&gt; that happens in software. New ideas are recycled and rediscovered every few years and now is a great time to hop along for the ride.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Turbolinks, Stimulus, and Server Rendered HTML is a compelling alternative to modern JavaScript single page apps. Let&apos;s build a hovercard to see how you can kick it old school with a more boring approach.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/stimulus-hovercard.png" />
          <media:content medium="image" url="https://boringrails.com/images/stimulus-hovercard.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Writing better StimulusJS controllers</title>
        <link href="https://boringrails.com/articles/better-stimulus-controllers/" rel="alternate" type="text/html" title="Writing better StimulusJS controllers" />
        <published>2020-06-01T13:00:00+00:00</published>
        <updated>2020-06-01T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/better-stimulus-controllers</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/better-stimulus-controllers/">&lt;blockquote&gt;
  &lt;p&gt;We write a lot of JavaScript at Basecamp, but we don’t use it to create “JavaScript applications” in the contemporary sense. All our applications have server-side rendered HTML at their core, then add sprinkles of JavaScript to make them sparkle. - DHH&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In early 2018, Basecamp released &lt;a href=&quot;https://m.signalvnoise.com/stimulus-1-0-a-modest-javascript-framework-for-the-html-you-already-have/&quot;&gt;StimulusJS into the world&lt;/a&gt;. &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; closed the loop on the “Basecamp-style” of building Rails applications.&lt;/p&gt;

&lt;p&gt;It’s hard to pin down a name for this stack, but the basic approach is a vanilla Rails app with server-rendered views, Turbolinks (“HTML-over-the-wire”, &lt;a href=&quot;https://github.com/defunkt/jquery-pjax&quot;&gt;pjax&lt;/a&gt;) for snappy page loads, and finally, Stimulus to “sprinkle” interactive behavior on top of your boring old HTML pages.&lt;/p&gt;

&lt;p&gt;Many of the tenets of Basecamp and DHH’s approach to building software weave in-and-out of this stack:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://rubyonrails.org/doctrine/#optimize-for-programmer-happiness&quot;&gt;Programmer Happiness&lt;/a&gt;: avoiding the ever-changing quicksand of “modern” JavaScript&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://m.signalvnoise.com/the-majestic-monolith/&quot;&gt;Majestic Monoliths&lt;/a&gt;: eschewing SPAs and microservices for medium-to-large Rails apps&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.com/signal-v-noise/threes-company-df77db78d1af&quot;&gt;Small teams doing big things&lt;/a&gt;: conceptual compression and tooling so you can build apps with 5 people, not 50&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://rubyonrails.org/doctrine/#omakase&quot;&gt;Omakase&lt;/a&gt;: tools that are good alone, but amazing together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And frankly, the most compelling to me: the tradition of extracting code from real-world products (and not trying to &lt;a href=&quot;https://twitter.com/nntaleb/status/750488970425954304&quot;&gt;lecture birds how to fly&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I’m excited to see more refinement of this stack as Basecamp prepares to launch &lt;a href=&quot;https://hey.com/&quot;&gt;HEY&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the coming months, we should see the release of Stimulus 2.0 to sharpen the APIs, a reboot of Server-generated JavaScript Responses (&lt;a href=&quot;https://signalvnoise.com/posts/3697-server-generated-javascript-responses&quot;&gt;SJR&lt;/a&gt;), and a splash of web-sockets to snap everything together.&lt;/p&gt;

&lt;p&gt;These techniques are extremely powerful, but require seeing the whole picture. Folks looking to dive into this stack (and style of development) will feel the &lt;a href=&quot;https://rubyonrails.org/doctrine/#provide-sharp-knives&quot;&gt;“Rails as a Sharp Knife” metaphor&lt;/a&gt; more so than usual.&lt;/p&gt;

&lt;p&gt;But I’ve been in the kitchen for a while and will help you make nice julienne cuts (and not slice off your thumb).&lt;/p&gt;

&lt;p&gt;Server-rendered views in Rails are a known path. Turbolinks, with a few caveats, is pretty much a drop-in and go tool these days.&lt;/p&gt;

&lt;p&gt;So today, I’ll be focusing on how to &lt;strong&gt;write better Stimulus controllers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article is explicitly not an introduction to Stimulus. The official &lt;a href=&quot;https://stimulus.hotwired.dev/reference/controllers&quot;&gt;documentation&lt;/a&gt; and &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Handbook&lt;/a&gt; are excellent resources that I will not be repeating here.&lt;/p&gt;

&lt;p&gt;And if you’ve never written any Stimulus controllers, the lessons I want to share here may not sink in right away. I know because they didn’t sink in for me!&lt;/p&gt;

&lt;p&gt;It took 18 months of living full-time in a codebase using this stack before things started clicking. Hopefully, I can help cut down that time for you. Let’s begin!&lt;/p&gt;

&lt;h2 id=&quot;what-may-go-wrong&quot;&gt;What may go wrong&lt;/h2&gt;

&lt;p&gt;The common failure paths I’ve seen when getting started with Stimulus:&lt;/p&gt;

&lt;h3 id=&quot;making-controllers-too-specific-either-via-naming-or-functionality&quot;&gt;Making controllers too specific (either via naming or functionality)&lt;/h3&gt;

&lt;p&gt;It’s tempting to start out writing one-to-one Stimulus controllers for each page or section where you want JavaScript. Especially if you’ve used React or Vue for your entire application view-layer. This is generally not the best way to go with Stimulus.&lt;/p&gt;

&lt;p&gt;It will be hard to write beautifully composable controllers when you first start. That’s okay.&lt;/p&gt;

&lt;h3 id=&quot;trying-to-write-react-in-stimulus&quot;&gt;Trying to write React in Stimulus&lt;/h3&gt;

&lt;p&gt;Stimulus is not React. React is not Stimulus. Stimulus works best when we let the server do the rendering. There is no virtual DOM or reactive updating or passing “data down, actions up”.&lt;/p&gt;

&lt;p&gt;Those patterns are not wrong, just &lt;em&gt;different&lt;/em&gt; and trying to shoehorn them into a Turbolinks/Stimulus setup will not work.&lt;/p&gt;

&lt;h3 id=&quot;growing-pains-weaning-off-jquery&quot;&gt;Growing pains weaning off jQuery&lt;/h3&gt;

&lt;p&gt;Writing idiomatic ES6 can be a stumbling block for people coming from the old days of jQuery.&lt;/p&gt;

&lt;p&gt;The native language has grown leaps and bounds, but you’ll still scratch your head from time to time wondering if people really think that:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.item&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;is an improvement on &lt;code class=&quot;highlighter-rouge&quot;&gt;$(&apos;.item&apos;)&lt;/code&gt;. (I’m right there with you, but &lt;a href=&quot;https://twitter.com/_swanson/status/1242909742793666562&quot;&gt;I digress…&lt;/a&gt;)&lt;/p&gt;

&lt;h2 id=&quot;how-to-write-better-stimulus-controllers&quot;&gt;How to write better Stimulus controllers&lt;/h2&gt;

&lt;p&gt;After taking Stimulus for a test drive and making a mess, I revisited the Handbook and suddenly I saw the examples in a whole new light.&lt;/p&gt;

&lt;p&gt;For instance, the Handbook shows an example for lazy loading HTML:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;content-loader&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-content-loader-url=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/messages.html&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  Loading...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice the use of &lt;code class=&quot;highlighter-rouge&quot;&gt;data-content-loader-url&lt;/code&gt; to pass in the URL to lazily load.&lt;/p&gt;

&lt;p&gt;The key idea here is that you aren’t making a &lt;code class=&quot;highlighter-rouge&quot;&gt;MessageList&lt;/code&gt; component. You are making a generic async loading component that can render any provided URL.&lt;/p&gt;

&lt;p&gt;Instead of the mental model of extracting page components, you go up a level and build “primitives” that you can glue together across multiple uses.&lt;/p&gt;

&lt;p&gt;You could use this same controller to lazy load a section of a page, or each tab in a tab group, or in a server-fetched modal when hovering over a link.&lt;/p&gt;

&lt;p&gt;You can see real-world examples of this technique on sites like GitHub.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note that GitHub does not use Stimulus directly, but the concept is identical)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/github-lazy-load.png&quot; alt=&quot;GitHub Lazy load&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The GitHub activity feed first loads the shell of the page and then makes an AJAX call that fetches more HTML to inject into the page.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Snippet from github.com --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;js-dashboard-deferred&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/dashboard-feed&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-priority=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GitHub uses the same deferred loading technique for the “hover cards” across the site.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/github-hover-card.gif&quot; alt=&quot;Github Hover Card&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Snippet from github.com --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;a&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-hovercard-type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-hovercard-url=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/users/swanson/hovercard&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/swanson&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  swanson
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By making general-purpose controllers, you start the see the true power of Stimulus.&lt;/p&gt;

&lt;p&gt;Level one is an opinionated, more modern version of jQuery &lt;code class=&quot;highlighter-rouge&quot;&gt;on(&quot;click&quot;)&lt;/code&gt; functions.&lt;/p&gt;

&lt;p&gt;Level two is a set of “behaviors” that you can use to quickly build out interactive sprinkles throughout your app.&lt;/p&gt;

&lt;h3 id=&quot;example-toggling-classes&quot;&gt;Example: toggling classes&lt;/h3&gt;

&lt;p&gt;One of the first Stimulus controllers you’ll write is a “toggle” or “show/hide” controller. You’re yearning for the simpler times of wiring up a click event to call &lt;code class=&quot;highlighter-rouge&quot;&gt;$(el).hide()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your implementation will look something like this:&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// toggle_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;contentTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And you would use it like so:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle#toggle&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Toggle&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle.content&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Some special content&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To apply the lessons about building more configurable components that the Handbook recommends, rework the controller to not hard-code the CSS class to toggle.&lt;/p&gt;

&lt;p class=&quot;pro-tip&quot;&gt;This will become even more apparent in the upcoming &lt;a href=&quot;https://github.com/stimulusjs/stimulus/pull/202&quot;&gt;Stimulus 2.0&lt;/a&gt; release when “classes” have a dedicated API.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// toggle_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;contentTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)));&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The controller now supports multiple targets and a configurable CSS class to toggle.&lt;/p&gt;

&lt;p&gt;You’ll need to update the usage to:&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-toggle-class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle#toggle&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Toggle&lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle.content&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Some special content&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This might seem unnecessary on first glance, but as you find more places to use this behavior, you may want a different class to be toggled.&lt;/p&gt;

&lt;p&gt;Consider the case when you also needed some basic tabs to switch between content.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-toggle-class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;active&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;tab active&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click-&amp;gt;toggle#toggle&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle.content&quot;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    Tab One
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;tab&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click-&amp;gt;toggle#toggle&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle.content&quot;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    Tab Two
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can use the same code. New feature, but no new JavaScript! The dream!&lt;/p&gt;

&lt;h3 id=&quot;example-filtering-a-list-of-results&quot;&gt;Example: filtering a list of results&lt;/h3&gt;

&lt;p&gt;Let’s work through another common example: filtering a list of results by specific fields.&lt;/p&gt;

&lt;p&gt;In this case, users want to filter a list of shoes by brand, price, or color.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/sneaker-filter.png&quot; alt=&quot;Filter Sneakers screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We’ll write a controller to take the input values and append them to the current URL as query parameters.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Base URL: /app/shoes
Filtered URL: /app/shoes?brand=nike&amp;amp;price=100&amp;amp;color=6
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This URL scheme makes it really easy to filter the results on the backend with Rails.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// filters_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pathname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;Turbolinks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;clearCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;Turbolinks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;visit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`brand=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;brandTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`price=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;priceTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`color=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;colorTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will work, but it’s not reusable outside of this page. If we want to apply the same type of filtering to a table of Orders or Users, we would have to make separate controllers.&lt;/p&gt;

&lt;p&gt;Instead, change the controller to handle arbitrary inputs and it can be reused in both places – especially since the inputs tags already have the &lt;code class=&quot;highlighter-rouge&quot;&gt;name&lt;/code&gt; attribute needed to construct the query params.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// filters_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;location&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pathname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;Turbolinks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;clearCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;Turbolinks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;visit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;filterTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;example-lists-of-checkboxes&quot;&gt;Example: lists of checkboxes&lt;/h3&gt;

&lt;p&gt;We’ve seen how to make controllers more reusable by passing in values and using generic targets. One other way is to use optional targets in your controllers.&lt;/p&gt;

&lt;p&gt;Imagine you need to build a &lt;code class=&quot;highlighter-rouge&quot;&gt;checkbox_list_controller&lt;/code&gt; to allow a user to check all (or none) of a list of checkboxes. Additionally, it needs an optional &lt;code class=&quot;highlighter-rouge&quot;&gt;count&lt;/code&gt; target to display the number of selected items.&lt;/p&gt;

&lt;p&gt;You can use the &lt;code class=&quot;highlighter-rouge&quot;&gt;has[Name]Target&lt;/code&gt; attribute to check for if the target exists and then conditionally take some action.&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// checkbox_list_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;checkAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setAllCheckboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;checkNone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setAllCheckboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;onChecked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;setAllCheckboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checkboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;checkbox&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;el&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checkbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;disabled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;checkbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checked&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;checked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;setCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;hasCountTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;selectedCheckboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;countTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;innerHTML&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; selected`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;selectedCheckboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checkboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checked&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;checkboxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;querySelectorAll&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;input[type=checkbox]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here we can use the controller to add “Check All” and “Check None” functionality to a basic form.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/email-preferences.gif&quot; alt=&quot;Email Preferences screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We can use the same code to build a checkbox filter that displays the count of the number of selections and a “Clear filter” button (“check none”).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/color-checkboxes.png&quot; alt=&quot;Color Checkbox Filter screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As with the other examples, you can see the power of creating Stimulus controllers that can be used in multiple contexts.&lt;/p&gt;

&lt;h2 id=&quot;putting-it-all-together-composing-multiple-controllers&quot;&gt;Putting it all together: composing multiple controllers&lt;/h2&gt;

&lt;p&gt;We can combine all three controllers to build a highly interactive multi-select checkbox filter.&lt;/p&gt;

&lt;p&gt;Here is a rundown of how it all works together:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use the &lt;code class=&quot;highlighter-rouge&quot;&gt;toggle_controller&lt;/code&gt; to show or hide the color filter options when clicking the input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/hide-controller.gif&quot; alt=&quot;Toggle controller example&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use the &lt;code class=&quot;highlighter-rouge&quot;&gt;checkbox_list_controller&lt;/code&gt; to keep the count of selected colors and add a “Clear filter” option&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/checkbox-controller.gif&quot; alt=&quot;Checkbox list example&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use the &lt;code class=&quot;highlighter-rouge&quot;&gt;filters_controller&lt;/code&gt; to update the URL when filter inputs change, for both basic HTML inputs and our multi-select filter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/filters-controller.gif&quot; alt=&quot;Filters example&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Each individual controller is simple and easy to implement but they can be combined to create more complicated behaviors.&lt;/p&gt;

&lt;p&gt;Here is the full markup for this example.&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filter-section&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filters&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filters&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filter-label&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Brand&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;select_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;options_from_collection_for_select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
              &lt;span class=&quot;no&quot;&gt;Shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;brands&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:to_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:to_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:brand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;include_blank: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;All Brands&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;form-select&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;filters#filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;target: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;filters.filter&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filter-label&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Price Range&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;select_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;options_for_select&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
              &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Under $100&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Under $200&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:price&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;include_blank: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Any Price&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;form-select&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;filters#filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;target: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;filters.filter&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filter-label&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Colorway&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;relative&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle checkbox-list&quot;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-select text-left&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle#toggle&quot;&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;checkbox-list.count&quot;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          All
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

        &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden select-popup&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;toggle.content&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex flex-col&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;select-popup-header&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;select-label&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Select colorways...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

              &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;clear-filters&quot;&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;checkbox-list#checkNone filters#filter&quot;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
                Clear filter
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

            &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;select-popup-list space-y-2&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Shoe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;colors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label_tag&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;leading-none flex items-center&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
                  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;check_box_tag&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;colors[]&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:colors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;include?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                    &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;form-checkbox text-indigo-500 mr-2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;target: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;filters.filter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
                  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

            &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;select-popup-action-footer&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;p-2 w-full select-none&quot;&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filters#filter&quot;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
                Apply
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&gt;Wrap it up&lt;/h2&gt;

&lt;p&gt;Stimulus works best when it’s used to add sprinkles of behavior to your existing HTML. Since Rails and Turbolinks are super effective at handling server-rendered HTML, these tools are a natural fit.&lt;/p&gt;

&lt;p&gt;Using Stimulus requires a change in mindset from both jQuery snippets and React/Vue. Think about adding behaviors, not about making full-fledged components.&lt;/p&gt;

&lt;p&gt;You’ll avoid the common stumbling blocks with Stimulus if you can make your controllers small, concise, and re-usable.&lt;/p&gt;

&lt;p&gt;You can compose multiple Stimulus controllers together to mix-and-match functionality and create more complex interactions.&lt;/p&gt;

&lt;p&gt;These techniques can be difficult to wrap your head around, but you can end up building highly interactive apps without writing much app-specific JavaScript at all!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/dhh-no-app-js.png&quot; alt=&quot;DHH tweet on not writing app-specific javascript&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s an exciting time as this stack evolves, more people find success with shipping software quickly, and it becomes a more known alternative to the “all-in on JavaScript SPA” approach.&lt;/p&gt;

&lt;h3 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.betterstimulus.com/&quot;&gt;Better StimulusJS&lt;/a&gt;: community site for emerging best practices&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/skatkov/awesome-stimulusjs&quot;&gt;Awesome StimulusJs&lt;/a&gt;: collection of links to articles, examples, podcasts&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://discourse.stimulus.hotwired.dev/&quot;&gt;Stimulus Community&lt;/a&gt;: low traffic, but features lots of snippets and thoughts from the core team&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/stimulus-use/stimulus-use&quot;&gt;stimulus-use&lt;/a&gt;: collection of composable behaviors for your controllers&lt;/li&gt;
&lt;/ul&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Stimulus sprinkles interactive behavior on top of your boring HTML pages. By keeping your controllers small, generic, and composable you can build a front-end without the typical JavaScript mess.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/better-stimulus.png" />
          <media:content medium="image" url="https://boringrails.com/images/better-stimulus.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Feature Flags: The stupid simple way to de-stress production releases</title>
        <link href="https://boringrails.com/articles/feature-flags-simplest-thing-that-could-work/" rel="alternate" type="text/html" title="Feature Flags: The stupid simple way to de-stress production releases" />
        <published>2020-04-12T13:00:00+00:00</published>
        <updated>2020-04-12T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/feature-flags-simplest-thing-that-could-work</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/feature-flags-simplest-thing-that-could-work/">&lt;blockquote&gt;
  &lt;p&gt;“So, uh, is it okay to deploy this?”&lt;br /&gt;
“Wait, hold on, that’s not ready to go-live!”&lt;br /&gt;
“Weird, they shouldn’t be able to see that yet…“&lt;br /&gt;
“Oh shit, the deploy failed, what do I do?”&lt;br /&gt;
“Update for today: I’ve been merging in changes to my feature branch”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Feature flags (or &lt;a href=&quot;https://en.wikipedia.org/wiki/Feature_toggle&quot;&gt;feature toggles&lt;/a&gt;) are a technique for incrementally rolling out functionality in an application. Work that is not completely ready to go live can be released in chunks while being hidden by a flag. These flags are usually configured per-environment so that you can, for instance, enable a feature on a staging site before turning it on in production.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://martinfowler.com/articles/feature-toggles.html&quot;&gt;Feature Toggles by Martin Fowler&lt;/a&gt; is the canonical article on this practice and outlines a full taxonomy of feature toggles.&lt;/p&gt;

&lt;p&gt;Toggles are broken down into four categories:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Release toggles&lt;/strong&gt;: allow in-progress code to be shipped to production&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Experiment toggles&lt;/strong&gt;: support A/B testing multiple code-paths&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ops toggles&lt;/strong&gt;: panic button / circuit breakers&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Permission toggles&lt;/strong&gt;: changing product features (premium, early access, etc)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/fowler-toggle-2x2.png&quot; alt=&quot;Martin Fowler: Feature Toggle Chart&quot; /&gt;&lt;/p&gt;

&lt;p class=&quot;caption&quot;&gt;Martin Fowler: Feature Toggle Chart&lt;/p&gt;

&lt;p&gt;Each type of toggle has different needs and properties. For most small to medium sized apps, one type stands above the others in terms of value: release toggles.&lt;/p&gt;

&lt;p class=&quot;do-this&quot;&gt;Generally, long running branches are a self-imposed disaster for teams that want to ship fast. The stock answer to “how do we avoid long running branches” is to do continuous integration – and &lt;strong&gt;feature flags are often the missing conceptual link&lt;/strong&gt; needed to bridge the textbook concept of CI with the practicality of “okay, how do we, you know, actually do that?”.&lt;/p&gt;

&lt;p&gt;While the other types of toggles are more context specific, every app can benefit from simple release toggles.&lt;/p&gt;

&lt;h2 id=&quot;how-should-we-use-feature-flags&quot;&gt;How should we use feature flags?&lt;/h2&gt;

&lt;p&gt;Factors that determine if feature flags are a joy or a rats nest of complexity:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Quantity&lt;/strong&gt;: how many flags do you have running at a time?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Duration&lt;/strong&gt;: how long are these flags hanging around?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Urgency&lt;/strong&gt;: how quickly would you need to turn a flag on/off?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Depth&lt;/strong&gt;: at what application level does the flag need to exist?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Risk&lt;/strong&gt;: how thoroughly do you need to flag a feature?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These factors are intertwined and have cascading effects.&lt;/p&gt;

&lt;p&gt;If you only have features flagged for a few days, you may be willing to accept more risk in partially exposing a feature.&lt;/p&gt;

&lt;p&gt;If your deploy process is currently slow, you’ll want to pick a solution that allows changing flags at runtime.&lt;/p&gt;

&lt;p&gt;If you have one or two flags at a time, are working on new features that can be hidden at a high level, and someone manually guessing the URL of unreleased features isn’t a problem, you can literally get away with glorified &lt;code class=&quot;highlighter-rouge&quot;&gt;if&lt;/code&gt; statements.&lt;/p&gt;

&lt;p class=&quot;pro-tip&quot;&gt;It’s always best to start small. As Sandi Metz would say: &lt;em&gt;“The future is uncertain and you will never know less than you know right now”&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Keep your options open and do the simplest thing that could possibly work and build from there (if you ever need to).&lt;/p&gt;

&lt;h2 id=&quot;how-should-we-implement-feature-flags&quot;&gt;How should we implement feature flags?&lt;/h2&gt;

&lt;p&gt;There are two approaches to implement feature flags: basic conditionals and “feature manager” libraries.&lt;/p&gt;

&lt;h3 id=&quot;basic-conditionals&quot;&gt;Basic Conditionals&lt;/h3&gt;

&lt;p&gt;Feature flags are, at their core, an &lt;code class=&quot;highlighter-rouge&quot;&gt;if&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;You can wrap up a very simple class to make the developer experience better and to keep your features organized. For getting started, don’t bother with any gems or external tools. Simply add a class like this to your Rails project.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Feature&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enabled?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;feature_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;feature_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_sym&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:meeting_transcripts&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;production?&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enabled?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:meeting_transcripts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Do your thing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Take advantage of established patterns like &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.env&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;Current.user&lt;/code&gt; to write simple conditionals to return true or false for any given feature. Since you completely own this code, you can adapt it however you want.&lt;/p&gt;

&lt;p&gt;This seems too primitive for the real-world, but even this kind of stupid simple “disabled in production” flag is often the only kind of logic you’ll need.&lt;/p&gt;

&lt;h3 id=&quot;pushing-the-limits&quot;&gt;Pushing the Limits&lt;/h3&gt;

&lt;p&gt;If you do want to further push the boundaries, you can try some of these clever hacks:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Current.user.admin? || !Rails.env.production&lt;/code&gt; - on for everyone in test, but only admins in production&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Current.account.early_access_enabled?&lt;/code&gt; - add fields to domain models for “opt-in” groups&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Current.user.email.contains(&quot;s&quot;)&lt;/code&gt; - roughly split a group in half using this hack from email marketing&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;Current.user.id % 100 &amp;lt; 10&lt;/code&gt; - roll out a feature to roughly 10% of users (it’s not a proper random sampling but you’re not actually going to do real statistical analysis anyways)&lt;/p&gt;

&lt;p&gt;These advanced conditionals are clever, but &lt;em&gt;clever&lt;/em&gt; is not inherently good. Prefer more boring per-environment flags whenever possible.&lt;/p&gt;

&lt;p&gt;Remember that we are primarily using these flags as &lt;strong&gt;release toggles&lt;/strong&gt;. If you’re looking to do actual data-driven analysis (which requires more statistical rigor than reading a blog post…) or expect the logic to be a permanent part of your application, you should look elsewhere.&lt;/p&gt;

&lt;h2 id=&quot;feature-management-tooling&quot;&gt;Feature management tooling&lt;/h2&gt;

&lt;p&gt;The other route teams take is to manage flags at run-time with a gem like &lt;a href=&quot;https://github.com/jnunemaker/flipper&quot;&gt;flipper&lt;/a&gt; or use a hosted service like &lt;a href=&quot;https://www.flippercloud.io/&quot;&gt;Flipper Cloud&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/flipper-ui.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p class=&quot;caption&quot;&gt;Sadly, no product manager has ever been able to understand this…&lt;/p&gt;

&lt;p&gt;There are some other gems (&lt;a href=&quot;https://github.com/FetLife/rollout&quot;&gt;rollout&lt;/a&gt;, &lt;a href=&quot;https://github.com/voormedia/flipflop&quot;&gt;flipflop&lt;/a&gt;, etc), but conceptually, they all end up using Redis as a key-value store, apply a bit of logic (percentage calculations, groupings, etc), and return true/false.&lt;/p&gt;

&lt;p&gt;In exchange for the additional machinery, you gain the ability to turn features on and off while the app is running or even have non-developers manage the roll-out.&lt;/p&gt;

&lt;p&gt;In reality, I’ve found that I rarely needed either of these features. Heavier feature flagging tools are both overkill for a simple cases (0-3 flags) and hard to manage if you have 5+ flags (which features are on? how do they interact? why is this off on staging, but on in production?).&lt;/p&gt;

&lt;p&gt;With flipper, the deployment process becomes decoupled from your normal development workflow. Depending on your context, this may be a positive or a negative.&lt;/p&gt;

&lt;p&gt;The stupid-simple &lt;code class=&quot;highlighter-rouge&quot;&gt;Feature&lt;/code&gt; class requires a code commit to change, which is slightly annoying but then hands-off. Using &lt;code class=&quot;highlighter-rouge&quot;&gt;flipper&lt;/code&gt; requires you to pull up the admin page in the right environment and click some buttons, or remote in and remember to flip some bits in a &lt;code class=&quot;highlighter-rouge&quot;&gt;rails console&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;where-should-you-put-feature-flags&quot;&gt;Where should you put feature flags?&lt;/h2&gt;

&lt;p&gt;Whenever possible, flag things at the “edges” of features. If you’re adding a new section, hide it in the navigation menu. If you’re building a new action on an existing page, hide the button.&lt;/p&gt;

&lt;p&gt;The fewer flags you need to put in your codebase at a given time, the easier it will be to manage.&lt;/p&gt;

&lt;p&gt;If you can get away with it, simply hide things at the view level. For 99% of users, if something doesn’t exist in the UI, it doesn’t exist in the app. Hell, it’s often hard enough to get users to notice new features that aren’t hidden!&lt;/p&gt;

&lt;p&gt;A “loosely” flagged feature will allow you to manually key in the URL if you want to run a quick test in an environment that isn’t officially enabled.&lt;/p&gt;

&lt;p&gt;But if you are changing some internal logic, or if there are risks with allowing users to potentially access features that in still underdevelopment, you’ll need to move your flags closer to the “core” of the system.&lt;/p&gt;

&lt;p&gt;Adding flags deep in a controller, model, or service is fine, but should be used only when necessary. Remember the same “keep it stupid simple” approach even as you move farther from the edges.&lt;/p&gt;

&lt;p&gt;For instance, if you want to 404 all endpoints for a new feature, you can drop in this 20-line concern to avoid sprinkling conditions everywhere.&lt;/p&gt;

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

  &lt;span class=&quot;n&quot;&gt;class_methods&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;attr_reader&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:feature_flaggable_name&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;feature_flag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;feature_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;vi&quot;&gt;@feature_flaggable_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;feature_name&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;included&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;prepend_before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:enforce_feature_flag!&lt;/span&gt;

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

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;enforce_feature_flag!&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;feature_flaggable_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ArgumentError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;No feature flag specified! &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Please call `feature_flag(:some_flag)` in &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;enabled?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;feature_flaggable_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AbstractController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ActionNotFound&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;FeatureFlaggableController&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;feature_flag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:posts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

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

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

&lt;h2 id=&quot;how-do-you-roll-out-features-with-this-basic-approach&quot;&gt;How do you roll-out features with this basic approach?&lt;/h2&gt;

&lt;p&gt;Rolling out features can be stressful, but by using feature flags in an incremental way, you can make your releases calmer.&lt;/p&gt;

&lt;p&gt;I generally try to follow these steps:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Add a feature flag when starting work on a big feature (default to “on” in non-production environments)&lt;/li&gt;
  &lt;li&gt;As functionality is ready, merge frequently and deploy behind the flag&lt;/li&gt;
  &lt;li&gt;With the flag still on, deploy finished feature to production and test / check things out&lt;/li&gt;
  &lt;li&gt;Update the feature logic to always return true, deploy and observe&lt;/li&gt;
  &lt;li&gt;If everything looks good, delete the feature logic completely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not hard and fast rules, but the key concepts are to avoid long-running branches and high pressure deploys where a ton of code is immediately going live in production for the first time.&lt;/p&gt;

&lt;p&gt;It may seem like it would slow you down, but for most features it’s only adding a few minutes of extra time. It only takes one production rollout screw up to burn you before you’ll start using release flags for any medium or large features.&lt;/p&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&gt;Wrap It Up&lt;/h2&gt;

&lt;p&gt;Shipping fast means deploying frequently and hiding in-progress work behind feature flags lets you safely rollout changes in a more controlled way. No more big “hope we don’t run into problems” deploys that create high stress situations. No more long running feature branches.&lt;/p&gt;

&lt;p&gt;Feature flags bridge the gap between the abstract concept of continuous delivery and tactical release of features. While there are many kinds of feature toggles, simple release flags provide the best benefit-to-cost ratio.&lt;/p&gt;

&lt;p&gt;You can achieve calmer deployments by making feature roll-outs a trivial event. Start small and use a boring &lt;code class=&quot;highlighter-rouge&quot;&gt;Feature&lt;/code&gt; class before investing in fancy, enterprise-grade feature management tooling.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Feature flags bridge the gap between the abstract concept of continuous delivery and tactical release of features. Start small with a glorified if-statement before adding more complicated tooling to get the most bang for your buck.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/feature-flags.png" />
          <media:content medium="image" url="https://boringrails.com/images/feature-flags.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Spring Cleaning: Tidying up your codebase</title>
        <link href="https://boringrails.com/articles/spring-cleaning/" rel="alternate" type="text/html" title="Spring Cleaning: Tidying up your codebase" />
        <published>2020-03-16T13:00:00+00:00</published>
        <updated>2020-03-16T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/spring-cleaning</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/spring-cleaning/">&lt;p&gt;As the weather warms up, I get energized to give my codebase a spring cleaning. I’ve worked on projects where the mess was so bad that we were afraid to touch anything and I’ve worked on projects where I aggressively tried to get to &lt;a href=&quot;https://mdswanson.com/blog/2015/10/26/code-neutral.html&quot;&gt;code neutral&lt;/a&gt; (deleting more lines of code than I added). But the right balance lies somewhere in the middle.&lt;/p&gt;

&lt;p&gt;Over the years, I’ve found a few tasks that I think provide the biggest bang for the buck. Easy, low risk things you can do in under an hour to make your codebase a little bit more inhabitable. Do one every day for a week, or go all-out one Friday afternoon and see how much you can finish.&lt;/p&gt;

&lt;h2 id=&quot;tidy-up-your-dependencies&quot;&gt;Tidy Up Your Dependencies&lt;/h2&gt;

&lt;p&gt;Tools like &lt;code class=&quot;highlighter-rouge&quot;&gt;bundler&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;yarn&lt;/code&gt; generate a bunch of files and folders for your project, but most of the mess is “out of sight, out of mind” as they shuffle things under the proverbial rug of &lt;code class=&quot;highlighter-rouge&quot;&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And we’re not even talking about &lt;em&gt;unused&lt;/em&gt; dependencies, just old versions of libraries sitting around since you’ve upgraded or tried out new tools in a branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundler&lt;/strong&gt;: Run &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle clean&lt;/code&gt; to &lt;a href=&quot;https://bundler.io/man/bundle-clean.1.html&quot;&gt;remove unused gems&lt;/a&gt; from your bundler directory.&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;➜ bundle clean &lt;span class=&quot;nt&quot;&gt;--dry-run&lt;/span&gt;
Would have removed rails-html-sanitizer &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.2.0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed jekyll-sitemap &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.3.1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed que &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.14.3&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed aws-sdk-kms &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.28.0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed actioncable &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;6.0.2.1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed rake &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;12.3.2&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed administrate &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.12.0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed factory_bot &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;5.1.0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed sass &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3.7.4&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed html-pipeline &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2.12.3&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed minitest &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;5.11.3&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed uniform_notifier &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.12.1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed parallel &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.19.1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Would have removed colorator &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.1.0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One caveat is that if you use system-level gems and have multiple Ruby projects, running &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle clean&lt;/code&gt; will try to remove global gems not used in your current project. This is probably not what you want.&lt;/p&gt;

&lt;p&gt;To avoid this, switch to using per-project bundles. You can do &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle install --path vendor/bundle&lt;/code&gt; to install gems to a project-specific folder and then run &lt;code class=&quot;highlighter-rouge&quot;&gt;bundle clean&lt;/code&gt; to remove unused gems for the project and not mess with gems for other projects.&lt;/p&gt;

&lt;p class=&quot;do-this&quot;&gt;&lt;strong&gt;Tip&lt;/strong&gt;: You can disable downloading gem documentation locally by adding &lt;code class=&quot;highlighter-rouge&quot;&gt;gem: -​-no-document&lt;/code&gt; to &lt;code class=&quot;highlighter-rouge&quot;&gt;~/.gemrc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yarn&lt;/strong&gt;: &lt;code class=&quot;highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; has a bad reputation for ballooning in size. As the nested folders get larger and larger, you can even start running into &lt;a href=&quot;https://stackoverflow.com/questions/28175200/how-to-delete-node-modules-deep-nested-folder-in-windows&quot;&gt;OS level limitations&lt;/a&gt; when trying to delete this massive pile of JavaScript.&lt;/p&gt;

&lt;p&gt;Run &lt;code class=&quot;highlighter-rouge&quot;&gt;yarn autoclean --init&lt;/code&gt; to generate a template file that slims down your &lt;code class=&quot;highlighter-rouge&quot;&gt;node_modules&lt;/code&gt; folder by removing cruft like test files, markdown files, and other miscellaneous junk that sneaks into the published packages.&lt;/p&gt;

&lt;p&gt;After adding a &lt;code class=&quot;highlighter-rouge&quot;&gt;.yarnclean&lt;/code&gt; file, the scrubbing process will run every time you add or install packages (and you can also run it manually).&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;➜ yarn autoclean &lt;span class=&quot;nt&quot;&gt;--init&lt;/span&gt;
yarn autoclean v1.17.0
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;1/1] Creating &lt;span class=&quot;s2&quot;&gt;&quot;.yarnclean&quot;&lt;/span&gt;...
info Created &lt;span class=&quot;s2&quot;&gt;&quot;.yarnclean&quot;&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; Please review the contents of this file &lt;span class=&quot;k&quot;&gt;then &lt;/span&gt;run &lt;span class=&quot;s2&quot;&gt;&quot;yarn autoclean --force&quot;&lt;/span&gt; to perform a clean.

➜ yarn autoclean &lt;span class=&quot;nt&quot;&gt;--force&lt;/span&gt;
yarn autoclean v1.17.0
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;1/1] Cleaning modules...
info Removed 4799 files
info Saved 17.75 MB.
✨  Done &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;4.37s.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p class=&quot;pro-tip&quot;&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Yarn automatically prunes extraneous packages whenever you run the install command so no need to do it yourself.&lt;/p&gt;

&lt;p&gt;If you’re really feeling ambitious, audit your dependencies to see if any can be removed. Mike Perham’s excellent &lt;a href=&quot;https://www.mikeperham.com/2016/02/09/kill-your-dependencies/&quot;&gt;Kill Your Dependencies&lt;/a&gt; article has a checklist to use when evaluating external libraries:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Every dependency in your application has the potential to bloat your app, to destabilize your app, to inject odd behavior via monkeypatching or buggy native code. When you are considering adding a dependency to your Rails app, it’s a good idea to do a quick sanity check, in order of preference:&lt;/p&gt;

  &lt;ol&gt;
    &lt;li&gt;Do I really need this at all? Kill it. [Uninstall the gem]&lt;/li&gt;
    &lt;li&gt;Can I implement the required minimal functionality myself? Own it. [Copy/vendor the code]
If you need a gem:&lt;/li&gt;
    &lt;li&gt;Does the gem have a native extension? Look for pure ruby alternatives. [Switch gems]&lt;/li&gt;
    &lt;li&gt;Does the gem transitively pull in a lot of other gems? Look for simpler alternatives. [Switch gems]&lt;/li&gt;
  &lt;/ol&gt;

  &lt;p&gt;Gems with native extensions can destabilize your system; they can be the source of mysterious bugs and crashes. Avoid gems which pull in more dependencies than their value warrants. Example of a bad gem: the fog gem which pulls in 39 gems, more dependencies than rails itself and most of which are unnecessary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;prune-your-git-branches&quot;&gt;Prune your git branches&lt;/h2&gt;

&lt;p&gt;If running &lt;code class=&quot;highlighter-rouge&quot;&gt;git branch -r&lt;/code&gt; fills your terminal with the ghosts of features past, you should clean things up! Keeping your branches tidy makes it easy to see which branches are open without scrolling through tens (or hundreds!) of lines.&lt;/p&gt;

&lt;p&gt;Run &lt;code class=&quot;highlighter-rouge&quot;&gt;git remote prune origin&lt;/code&gt; to delete local tracking branches that don’t exist on origin. You can use &lt;code class=&quot;highlighter-rouge&quot;&gt;--dry-run&lt;/code&gt; first if you’re worried.&lt;/p&gt;

&lt;p&gt;If you run &lt;code class=&quot;highlighter-rouge&quot;&gt;git branch -r&lt;/code&gt; again, you should see a slimmer list that only shows the branches that exist in GitHub.&lt;/p&gt;

&lt;p&gt;You can then either go to GitHub directly or use &lt;code class=&quot;highlighter-rouge&quot;&gt;git ls-remote --heads origin&lt;/code&gt; to list the current remote branches. If there are any to remove, delete them by running &lt;code class=&quot;highlighter-rouge&quot;&gt;git push origin -D BRANCH_TO_DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/github-delete-branches.png&quot; alt=&quot;Automatically delete head branches&quot; /&gt;&lt;/p&gt;

&lt;p class=&quot;do-this&quot;&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Turn on “Automatically delete head branches” in GitHub so your branches don’t hang around after you’ve merged a pull request&lt;/p&gt;

&lt;h2 id=&quot;remove-unused-routes-and-views&quot;&gt;Remove unused routes and views&lt;/h2&gt;

&lt;p&gt;While the Rails generators spark joy for me, they often generate more routes and views than you actually want. Apply your inner Marie Kondo and excise these unused parts of your app.&lt;/p&gt;

&lt;p&gt;First try searching your app for “Find me in”: this is the default generator text for some views that Rails has generated. If you have any results, congratulations! You can delete these files without impunity.&lt;/p&gt;

&lt;p&gt;Next up: check your &lt;code class=&quot;highlighter-rouge&quot;&gt;routes.rb&lt;/code&gt; file for unused routes by looking for controller actions that aren’t implemented (or are missing views).&lt;/p&gt;

&lt;p&gt;I found the best results from &lt;a href=&quot;https://gist.github.com/strzibny/4ccbda7dcf67ef6719dcb047014e1ea7&quot;&gt;this gist&lt;/a&gt;. Drop the script into the root of your Rails app and run it to see a list of unused routes to potentially clean up.&lt;/p&gt;

&lt;h2 id=&quot;checking-for-unused-database-tables-and-columns&quot;&gt;Checking for unused database tables and columns&lt;/h2&gt;

&lt;p&gt;A final area to check is your database, which may have accumulated unused tables or columns. One clever approach is to look for empty tables and columns that have the same value for every row. These are suspicious places to investigate.&lt;/p&gt;

&lt;p&gt;Paste this script (adapted from &lt;a href=&quot;http://blog.ianenders.com/coding/2013/07/02/detecting-unused-db-fields-in-rails.html&quot;&gt;this article&lt;/a&gt;) into your project to scan your database (you may want to run against a staging database if your development database does not have representative data):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require_relative&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;./config/environment.rb&apos;&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connection&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;collect&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SELECT count(1) as count FROM &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;count&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;TABLE UNUSED &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;collect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reject&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;id&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;SELECT DISTINCT(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;) AS val FROM &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; LIMIT 2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Distinct Check&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;val&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;COLUMN UNUSED &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;COLUMN SINGLE VALUE &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; -- &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;val&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;➜ ruby unused_db.rb
TABLE UNUSED active_storage_blobs
TABLE UNUSED friendly_id_slugs
TABLE UNUSED active_storage_attachments
COLUMN SINGLE VALUE investor_agreements:file_extension &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; pdf
COLUMN UNUSED clients:comments
COLUMN SINGLE VALUE client_tasks:archived &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In additional to some unused generated tables, the script also found dozens of columns with all &lt;code class=&quot;highlighter-rouge&quot;&gt;null&lt;/code&gt; values or all single values (e.g. an &lt;code class=&quot;highlighter-rouge&quot;&gt;archived&lt;/code&gt; column that is always “false”). These columns require more investigation to remove, but are a good list to start.&lt;/p&gt;

&lt;h2 id=&quot;check-for-missing-validations--constraints&quot;&gt;Check for missing validations / constraints&lt;/h2&gt;

&lt;p&gt;While ActiveRecord provides a validation layer on top of your database, you’ll still want the strong protection of database constraints (non-nullable columns, unique indices, etc) to make sure no bad data sneaks into your app.&lt;/p&gt;

&lt;p&gt;It’s really easy for your models and underlying database to get out-of-sync. Use the &lt;a href=&quot;https://github.com/djezzzl/database_consistency&quot;&gt;database_consistency&lt;/a&gt; gem to run a series of checks to tell you where your application models and database schema are out of sync.&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;➜ bundle &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;database_consistency
fail ProjectType name column is required &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the database but &lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;not have presence validator
fail ProjectType slug column is required &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the database but &lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;not have presence validator
fail Project draft_documents associated model should have proper index &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the database
fail Project attachments associated model should have proper index &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the database
fail Project alerts associated model should have proper index &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the database
fail Company name column should be required &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the database
fail PurchaseApproval &lt;span class=&quot;nb&quot;&gt;date &lt;/span&gt;column should be required &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the database
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You may be overwhelmed by the volume of errors this tool spits out for a project, but don’t fret, every change should be straightforward and you can handle them bit by bit. Once you resolve everything, consider adding this command to your &lt;a href=&quot;/articles/building-a-rails-ci-pipeline-with-github-actions/&quot;&gt;Rails CI pipeline&lt;/a&gt; in order to catch future errors right when the offending code is committed.&lt;/p&gt;

&lt;h2 id=&quot;clear-out-your-migrations-folder&quot;&gt;Clear out your migrations folder&lt;/h2&gt;

&lt;p&gt;The final place to check is your migrations. Over time, your &lt;code class=&quot;highlighter-rouge&quot;&gt;db/migrate&lt;/code&gt; folder will build up to hundreds of migration files. If you’ve already run these migrations on production environments (and if &lt;a href=&quot;/articles/rails-database-migrations-strategy-how-to-manage-migrations-without-losing-your-mind/&quot;&gt;you’re avoiding creating seed data directly in migrations&lt;/a&gt;), you can safely delete older migrations.&lt;/p&gt;

&lt;p&gt;I like the &lt;a href=&quot;https://medium.com/clutter-engineering/cleaning-up-old-rails-migrations-1b55b638abb5&quot;&gt;approach outlined by Clutter&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Delete migrations older than 3 months&lt;/li&gt;
  &lt;li&gt;Add a new migration that raises if your database was very out-of-date (with instructions on how to load the schema)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If deleting the migrations makes you uneasy, you can also look at &lt;a href=&quot;https://github.com/jalkoby/squasher&quot;&gt;squasher&lt;/a&gt;: a tool that combines your old migrations into one mega-migration.&lt;/p&gt;

&lt;h2 id=&quot;deep-cleaning-going-even-further&quot;&gt;Deep cleaning: going even further&lt;/h2&gt;

&lt;p&gt;If you’re ready to &lt;strong&gt;deep&lt;/strong&gt; clean, consider trying out these tools to help you dig further into your application code:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/joshuaclayton/unused&quot;&gt;unused&lt;/a&gt;: a general-purpose tool for finding dead code and is based on &lt;code class=&quot;highlighter-rouge&quot;&gt;ctags&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/seattlerb/debride&quot;&gt;debride&lt;/a&gt;: a Ruby tool to find potentially uncalled methods (with some specific Rails checks)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/danmayer/coverband&quot;&gt;coverband&lt;/a&gt;: a “run in production” coverage tool that collects stats on method usage; it’s the most thorough but has a longer turn around time as you need to run it for a while against real traffic&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://thoughtbot.com/blog/cleaning-up-with-rcov&quot;&gt;rcov&lt;/a&gt;: depending on your test suite, you can get a good picture of what parts of your application may not be used&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/julianrubisch/attractor&quot;&gt;attractor&lt;/a&gt;: metrics visualizer for plotting churn (how often code changes) vs complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven’t had as much success with these tools and generally don’t find them to be a good ROI, but if you’re looking to keep your codebase sparkling clean, it’s worth spending some time exploring. Personally, I’m okay with a little bit of dust :)&lt;/p&gt;

&lt;p&gt;Happy cleaning! If you’ve got any other tips that you’ve used on your projects, let me know on &lt;a href=&quot;https://twitter.com/_swanson&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">A practical checklist for tidying up your gems, pruning old git branches, removing unused views and routes, and cleaning up your database. A little bit goes a long way when it comes to cleaning!</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/spring-cleaning.png" />
          <media:content medium="image" url="https://boringrails.com/images/spring-cleaning.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Wrangling slow reports, large file exports, and long-running tasks in Rails with Active Job</title>
        <link href="https://boringrails.com/articles/large-exports-and-slow-reports-with-activejob/" rel="alternate" type="text/html" title="Wrangling slow reports, large file exports, and long-running tasks in Rails with Active Job" />
        <published>2020-01-26T13:00:00+00:00</published>
        <updated>2020-01-26T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/large-exports-and-slow-reports-with-activejob</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/large-exports-and-slow-reports-with-activejob/">&lt;iframe class=&quot;mx-auto mb-2&quot; width=&quot;420&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/7pIuB6ZvT3Q&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;&lt;/iframe&gt;

&lt;p class=&quot;text-center italic text-sm text-gray-500 mb-4 font-serif&quot;&gt;In case you don’t like reading (Feb 2020 - Indy.rb Meetup)&lt;/p&gt;

&lt;p&gt;In every non-trivial application you’ll have to do something that is just slow. You need to export a large data set to a CSV file. An analyst needs a complicated Excel report. Or maybe you have to connect to an external API and process a whole bunch of data.&lt;/p&gt;

&lt;p&gt;So how do you know when it’s time to use &lt;strong&gt;background jobs&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;If you try building these kinds of reports in a normal Rails controller action, you will see these symptoms:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Request timeouts&lt;/strong&gt;: once response times start creeping past 10+ seconds, you run the risk of timeouts. You might have a report that is slow but just barely finishes, but you know that it scale linearly with the amount of data and will break soon. Platforms like Heroku will kill requests after 30 seconds and, in the mean time, your whole app slows down as workers/dynos are tied up with long running actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/heroku-timeout-issue.png&quot; alt=&quot;Heroku Timeout Issue R12&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Memory spikes&lt;/strong&gt;: the most straightforward implementations for file exporting generate a file in memory before serving it to the user. For small files, this is fine; but if you create 100+ MB files in memory, your RAM usage will spike up and down wildly. If even a few users are doing exports at the same time, you will hit your resource cap and see failed requests and Heroku “R14 - Memory Quota Exceeded” errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/images/heroku-memory-issue.png&quot; alt=&quot;Heroku Memory Issue R14&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Bad UX&lt;/strong&gt;: even if your server weathers the extra load of slow actions, users may not be so patient. A never-ending loading indicator in the browser tab, or worse, nothing happening for 20 seconds is not a good experience. You can throw up a “Loading…” spinner, but can we do better?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first step is to buy some breathing room by optimizing the queries or fixing any obvious performance issues. Spend an hour looking through the &lt;a href=&quot;https://www.speedshop.co/blog/&quot;&gt;Speedshop Rails performance blog&lt;/a&gt; to see if there is low-hanging fruit.&lt;/p&gt;

&lt;p&gt;But once you start seeing these kind of problems, it’s time to set up infrastructure for background jobs.&lt;/p&gt;

&lt;h2 id=&quot;pick-your-tools&quot;&gt;Pick Your Tools&lt;/h2&gt;

&lt;p&gt;Adding the first background job is always the hardest. You’ve got frustrated users or an irate customer (that’s called an &lt;em&gt;acute need&lt;/em&gt; in business speak!), but you don’t know your long-term needs past this first use case.&lt;/p&gt;

&lt;p&gt;There are two initial decisions to make when setting up background jobs in Rails.&lt;/p&gt;

&lt;h3 id=&quot;active-job-or-not&quot;&gt;Active Job or Not?&lt;/h3&gt;

&lt;p&gt;Way back in Rails 4.2, &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_job_basics.html&quot;&gt;Active Job&lt;/a&gt; was released as a common abstraction over background job processing. Following in the footsteps of other Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveX&lt;/code&gt; gems, Active Job is an API that allows you add background jobs into queue and then use tools like &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html&quot;&gt;Sidekiq, Delayed Job, or Resque&lt;/a&gt; to “work” or process those jobs.&lt;/p&gt;

&lt;p&gt;Active Job defines a core set of functionality that should serve most applications really well: you can (obviously) create jobs, but also put jobs into separate queues (priorities), schedule jobs to run in the future, and handle exceptions. These are the basic primitives that you want for background jobs.&lt;/p&gt;

&lt;p&gt;As with all Rails abstractions, there are trade-offs: a standard interface allows for deeper framework integration but has to work with the lowest common denominator.&lt;/p&gt;

&lt;p&gt;Using Active Job affords you a simpler development environment. In development, you can set the queue adapter to &lt;code class=&quot;highlighter-rouge&quot;&gt;:async&lt;/code&gt; to run jobs with an &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html&quot;&gt;in-process thread pool&lt;/a&gt;. Run &lt;code class=&quot;highlighter-rouge&quot;&gt;bin/rails server&lt;/code&gt; locally and jobs will get worked without running a separate worker command or using a tool like &lt;code class=&quot;highlighter-rouge&quot;&gt;foreman&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The downsides are that if you need more powerful features or want to utilize an adapter specific pattern, you are fighting against the current.&lt;/p&gt;

&lt;p&gt;You can certainly set up your app to use Sidekiq directly and forgo Active Job completely. And if you need to use advanced features (special retry strategies, batching, pushing huge numbers of jobs, rate limiting, etc), there are benefits to being “closer to the metal” with the background processor (and some features may &lt;a href=&quot;https://github.com/mperham/sidekiq/wiki/Active-Job#commercial-features&quot;&gt;not work with Active Job at all&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Skipping Active Job can also give you a non-trival performance boost, &lt;a href=&quot;https://github.com/mperham/sidekiq/wiki/Active-Job#performance&quot;&gt;per the Sidekiq wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I recommend starting off with Active Job; you can always port later down the line when you have a clear vision of your long-term needs. If you hit limits or cannot make it work, you can justify spending more time tuning and going off the standard Rails path.&lt;/p&gt;

&lt;h3 id=&quot;delayed-job-or-sidekiq&quot;&gt;Delayed Job or Sidekiq?&lt;/h3&gt;

&lt;p&gt;The second consideration is which queueing adapter to use in production. There are only two options unless you have some extremely exotic needs: &lt;a href=&quot;https://github.com/mperham/sidekiq&quot;&gt;Sidekiq&lt;/a&gt; or &lt;a href=&quot;https://github.com/collectiveidea/delayed_job&quot;&gt;Delayed Job&lt;/a&gt;. These are both established libraries and have the best tutorials, articles, and extensions.&lt;/p&gt;

&lt;p&gt;Sidekiq is the current “default” option for many developers. It scales up well and the &lt;a href=&quot;https://sidekiq.org/products/pro.html&quot;&gt;Sidekiq Pro/Enterprise option&lt;/a&gt; is very reassuring for the continued stability and improvement of the project. Mike Perham (and the rest of the contributors) do a really nice job on the &lt;a href=&quot;https://github.com/mperham/sidekiq/wiki&quot;&gt;Sidekiq documentation and wikis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you plan on having thousands and thousands of jobs running per day, Sidekiq is a no-brainer.&lt;/p&gt;

&lt;p&gt;The one minor drawback to Sidekiq is that you take on a hard dependency on &lt;a href=&quot;https://redis.io/&quot;&gt;Redis&lt;/a&gt; to store the queue of jobs and metadata. If you already have Redis in your infrastructure (most likely for caching), this is a moot point. The majority of Rails developers are comfortable with having Redis in their tech stack and you can spin up hosted instances in about 5 minutes on all the major cloud platforms.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/sidekiq-vs-dj.png&quot; alt=&quot;Debating Sidekiq vs Delayed Job&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Delayed Job is the older, less shiny option. While it has fallen out of community favor in recent years, it is still a very solid gem. Delayed Job was originally extracted from Shopify’s internal codebase so it has been battle tested at high scale.&lt;/p&gt;

&lt;p&gt;Delayed Job works with ActiveRecord and uses your PostgresQL database for storing the jobs. No additional infrastructure or Heroku add-ons needed. It’s really easy to set up and there is one less moving part.&lt;/p&gt;

&lt;p&gt;Sharing your application’s database is also handy if you want to peek into the underlying table to debug or see how the tool works; it’s just another table.&lt;/p&gt;

&lt;p&gt;Because Delayed Job is single-threaded (compared to Sidekiq’s multi-threaded model) it is not advised for processing 100,000s of jobs per day. That said, many applications will never reach that scale! Delayed Job can take you pretty far with no extra infrastructure.&lt;/p&gt;

&lt;p&gt;By the time you feel the scaling pain, you may be ready for a much bigger engineering investment – or have a whole team of people to tune and manage background processing.&lt;/p&gt;

&lt;p&gt;If you have an (actually) large web service (Shopify/GitHub/Basecamp-esque loads), there is a whole range of “state of the art” patterns and tooling to consider (see &lt;a href=&quot;https://kirshatrov.com/2019/01/03/state-of-background-jobs/&quot;&gt;Kir Shatrov’s great writeup from the trenches at Shopify&lt;/a&gt;), but the rest of us will be best served with a boring choice.&lt;/p&gt;

&lt;p&gt;Ultimately, the decision comes down to if you already have Redis as part of your infrastructure. Both libraries work well and have similar set up time.&lt;/p&gt;

&lt;p&gt;Sidekiq will allow you to scale up more, but with a bit of infrastructure tax; Delayed Job will fall over if you are trying to crank though 5000 jobs/sec, but that should not be a primary concern for small/medium-sized applications. You can’t really go wrong with either option.&lt;/p&gt;

&lt;p&gt;In a recent project with a tiny team and a small amount of jobs (10s-100s per day), we went with Delayed Job for the simplicity and less moving pieces. It’s fine.&lt;/p&gt;

&lt;h2 id=&quot;the-basics-of-background-jobs&quot;&gt;The Basics of Background Jobs&lt;/h2&gt;

&lt;p&gt;At the highest level, using background jobs means taking your slow code and moving it in a &lt;code class=&quot;highlighter-rouge&quot;&gt;Job&lt;/code&gt; class. Instead of running the slow code in your controller, you enqueue the Job and pass it any input data it needs.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://guides.rubyonrails.org/active_job_basics.html&quot;&gt;Active Job guide&lt;/a&gt; does a good job explaining the general concept but there are a few best-practices that will give you an outsized benefit if you follow them.&lt;/p&gt;

&lt;p&gt;The top three things you can do to avoid headaches with jobs are: use Global ID, check pre-conditions, and split up large jobs.&lt;/p&gt;

&lt;h3 id=&quot;global-id&quot;&gt;Global ID&lt;/h3&gt;

&lt;p&gt;You may see advice for other tools that are the exact opposite of this tip, but in Rails, you can pass &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveRecord&lt;/code&gt; models as arguments to your jobs and they will Just Work. Under the hood, Rails uses a concept called “Global ID” to serialize a reference to a model object. If you have a &lt;code class=&quot;highlighter-rouge&quot;&gt;Company&lt;/code&gt; model with &lt;code class=&quot;highlighter-rouge&quot;&gt;id=5&lt;/code&gt;, Rails will generate an app wide unique id for the model (in this case: &lt;code class=&quot;highlighter-rouge&quot;&gt;gid://YourApp/Company/5&lt;/code&gt;).&lt;/p&gt;

&lt;p class=&quot;not-this force-mb&quot;&gt;&lt;strong&gt;Don’t:&lt;/strong&gt; Manually query models by ID&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CloseListingJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;listing_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;listing_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;archive!&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;notify_followers&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In other contexts, passing objects to background jobs is considered an anti-pattern as you may serialize an object that is “stale” when you go to run the job later. Rails will get a fresh copy of the object before your job runs so you don’t have to worry about it.&lt;/p&gt;

&lt;p&gt;If you’ve dealt with weird behavior from serialized/marshaled objects (&lt;em&gt;shudders&lt;/em&gt;), you probably have battle scars, but you don’t need to pass around IDs and manually re-fetch data anymore.&lt;/p&gt;

&lt;p class=&quot;do-this force-mb&quot;&gt;&lt;strong&gt;Do:&lt;/strong&gt; Use Global ID to automatically handle common serializing&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CloseListingJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;archive!&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;notify_followers&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;listing&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;check-pre-conditions&quot;&gt;Check Pre-Conditions&lt;/h3&gt;

&lt;p&gt;Two reasons to check pre-conditions for jobs (especially destructive actions): the world may have changed and the double-queuing problem.&lt;/p&gt;

&lt;p&gt;Since you’ve offloaded a task to the background, confirm that nothing major changed from the time we enqueued the job and when the code actually runs.&lt;/p&gt;

&lt;p&gt;Imagine a case where you get ready to send a late invoice email to a customer, it takes a few minutes for the job to get processed, and during that time the customer just so happens to submit their overdue payment. The job is already in the queue and without guarding against the invoice being paid, you’d send a confusing email out.&lt;/p&gt;

&lt;p&gt;The other issue is when multiple copies of the same job are enqueued. Maybe two users triggered the same job, maybe there is a bug in your code, or maybe a job got run twice when your dyno restarted. There is a fancy computer-science word for this: &lt;a href=&quot;http://joycse06.github.io/blog/2016/09/designing-good-background-jobs-idempotence/&quot;&gt;idempotence&lt;/a&gt;. Ensure the same job could run multiple times without negative side-effects.&lt;/p&gt;

&lt;p class=&quot;not-this force-mb&quot;&gt;&lt;strong&gt;Don’t:&lt;/strong&gt; Fire away destructive actions blindly&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NotifyLateInvoiceJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Fire off a scary email&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;InvoiceMailer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;invoice: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;late_reminder_email&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There are plugins and tools that can try to enforce idempotence (see gems like &lt;code class=&quot;highlighter-rouge&quot;&gt;sidekiq-unique-jobs&lt;/code&gt;), but in general, take precautions in your own application code.&lt;/p&gt;

&lt;p class=&quot;do-this force-mb&quot;&gt;&lt;strong&gt;Do:&lt;/strong&gt; Ensure the conditions that cause you to enqueue a job are true when it runs&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NotifyLateInvoiceJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Confirm that nothing has changed since the job was scheduled&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;late?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;notified?&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;InvoiceMailer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;invoice: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;late_reminder_email&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;# Track state for things that should only happen once&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;invoice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;notified: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;split-up-large-jobs&quot;&gt;Split Up Large Jobs&lt;/h3&gt;

&lt;p&gt;In addition to moving work off of the “main thread”, background queues allow you to split up and parallelize work. But just putting something in a background job does not make it run faster.&lt;/p&gt;

&lt;p&gt;Prefer many small jobs to one big job – this helps with performance (letting multiple workers crank through the task) and also helps in the event that the job is interrupted (dyno restart, unexpected errors, random bit flips due to &lt;a href=&quot;https://stackoverflow.com/questions/4109218/do-gamma-rays-from-the-sun-really-flip-bits-every-once-in-a-while&quot;&gt;solar flares&lt;/a&gt;).&lt;/p&gt;

&lt;p class=&quot;not-this force-mb&quot;&gt;&lt;strong&gt;Don’t:&lt;/strong&gt; Run excessively slow loops in a single job&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ComputeCompanyMetricsJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;employees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# A bunch of slow work&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_hours&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_productivity&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_usage&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You may hear this concept called &lt;a href=&quot;https://en.wikipedia.org/wiki/Fan-out_(software)&quot;&gt;“fanning out”&lt;/a&gt;: you take one job that might have 100 loop iterations and instead spin off 100 smaller jobs.&lt;/p&gt;

&lt;p&gt;If you have 10 workers available, you can finish the job ~10x faster (ignoring overhead) and if the 62nd loop iteration fails, you can retry that one small piece of work (instead of starting the whole thing over).&lt;/p&gt;

&lt;p class=&quot;do-this force-mb&quot;&gt;&lt;strong&gt;Do:&lt;/strong&gt; Enqueue smaller jobs from another job&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ComputeCompanyMetricsJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;employees&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Fan-out with parallel jobs for each employee&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;ComputeEmployeeMetricsJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;perform_later&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ComputeEmployeeMetricsJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_hours&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_productivity&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;employee&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_usage&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Don’t go overboard: it’s not advisable to break every job with a loop into sub-jobs. Sometimes it’s not worth the extra complexity or you want the work all wrapped in the same transaction.&lt;/p&gt;

&lt;p&gt;But if you find yourself creating a job where you are looping and doing more than a query or two, think about splitting up the job.&lt;/p&gt;

&lt;h2 id=&quot;getting-data-back-to-the-user&quot;&gt;Getting Data Back to the User&lt;/h2&gt;

&lt;p&gt;Most guides on background jobs make the faulty assumption that everything is “fire-and-forget”. Sure, for sending password reset emails, it’s fine to enqueue the job and finish the request. It’s unlikely to fail, takes only a few seconds, and you don’t need the result of the job to inform the user to kindly check their inbox.&lt;/p&gt;

&lt;p&gt;But what about generating a 20,000 row Excel file or a background process that takes 5 minutes to complete? We often need a way to report back progress and send back information once the job is completed.&lt;/p&gt;

&lt;p&gt;The fundamental issue is that when you move work to a background job, code execution does not stop and you need a way to “connect” back to the user that caused the job to be created. It’s outside of a single request-response life cycle so you cannot rely on normal Rails controllers.&lt;/p&gt;

&lt;p&gt;There are two approaches that are well suited for the vast majority of jobs: &lt;strong&gt;email later&lt;/strong&gt; and &lt;strong&gt;polling + progress&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id=&quot;email-later&quot;&gt;Email Later&lt;/h3&gt;

&lt;p&gt;One way to relay that a job has been completed is to send an email. The benefit is that it’s simple. You run a job and then off an &lt;code class=&quot;highlighter-rouge&quot;&gt;ActionMailer&lt;/code&gt; email with any relevant status (“Your contacts have now been imported!” or “Failed to sync calendar events. Contact support”) or links back into your app (“View your annual tax documents now”).&lt;/p&gt;

&lt;p&gt;You can see an example of this pattern in &lt;a href=&quot;https://mailchimp.com/&quot;&gt;Mailchimp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For reports that may take a long time (e.g. exporting the campaign statistics for every email you’ve ever sent), you start by clicking “Generate Report”&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/mailchimp-report-1.png&quot; alt=&quot;Mailchimp Example: Generate Report&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Mailchimp informs you that for large accounts, it may take a few minutes and to expect an email when the data is done exporting.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/mailchimp-report-2.png&quot; alt=&quot;Mailchimp Example: Generating Export&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After some time, you get an email from Mailchimp with a link to download the report.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/mailchimp-report-3.png&quot; alt=&quot;Mailchimp Example: Report Complete Email&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You are taken back to the Mailchimp app and can download a zip of your report.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/mailchimp-report-4.png&quot; alt=&quot;Mailchimp Example: Download Report&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Mailchimp opts to store the most recent file export in your account, in case you want to download the same report again later. If you generate another large report, it will overwrite the file.&lt;/p&gt;

&lt;p&gt;For your own application, you can decide if that approach to saving reports makes sense. You may opt to store a complete history of all files, keep the most recent three, or something else entirely.&lt;/p&gt;

&lt;p&gt;Here is some sample code to implement this workflow (specific details will need to adapted to your own app).&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReportsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generate_campaign_export&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Enqueue the job&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;CampaignExportJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;perform_later&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Display a message telling user to await an email&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:check_inbox&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CampaignExportJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Build the big, slow report into a zip file&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;zip&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ZipFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;campaigns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;campaign&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;CsvReport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;campaign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Store the report on a file system for downloading&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;file_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AwsService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;upload_s3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Record the location of the file&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;most_recent_report: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Notify the user that the download is ready&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;ReportMailer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;campaign_export&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deliver_later&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- application/views/report_mailer/campaign_export.html.erb --&amp;gt;&lt;/span&gt;

Hi &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;,

The export of your campaign report is complete. You can download it now.

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Download Report&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;download_campaign_export_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ReportsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;download_campaign_export&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Create a &apos;safe link&apos; to download (e.g. signed, expiring, etc)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;download_url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AwsService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;download_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;most_recent_report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Trigger a download in the browser&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;download_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;polling--progress&quot;&gt;Polling + Progress&lt;/h3&gt;

&lt;p&gt;For basic reports, emailing will suffice, but sometimes users need more frequent status updates on what’s happening in the background process.&lt;/p&gt;

&lt;p&gt;An alternative pattern is to combine polling (periodically checking on the job via AJAX calls) with progress reporting. One benefit of this approach is that it is super extendable. You can implement the progress reporting in a variety of ways: a numeric progress bar, a single “status” message, or even a detailed log of progress.&lt;/p&gt;

&lt;p&gt;This pattern is well suited for big user-generated reports, as well as long-running “system tasks” that the user may want to monitor.&lt;/p&gt;

&lt;p&gt;You can see an example of this pattern in the &lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify&lt;/a&gt; build dashboard.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/netlify-log-example.gif&quot; alt=&quot;Netlify Build Log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.heroku.com/&quot;&gt;Heroku&lt;/a&gt; uses the same pattern for displaying progress for the minutes-long process of deploying a new version of your software.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/heroku-log-example.png&quot; alt=&quot;Heroku Build Log&quot; /&gt;&lt;/p&gt;

&lt;p&gt;These UIs may look complicated, but at their core, it is a repeating AJAX call to check the job status and a block of text. You can reuse this pattern for multiple different reports in your application and make the output as detailed as you need.&lt;/p&gt;

&lt;p&gt;You can think of this pattern as &lt;strong&gt;building a CLI interface inside of a web page&lt;/strong&gt;. And since the main UI element is text, it’s really easy to build and change as needed.&lt;/p&gt;

&lt;p&gt;Here is some rough sample code to implement this workflow. This sample uses a separate database model for storing “job status” that is passed to the job and a &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; controller to handle client-side polling/updating.&lt;/p&gt;

&lt;p&gt;Specific details will need to be adapted to your own app.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MetricsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;export_company_stats&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;company&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:company_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;JobStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Company Stats - &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;no&quot;&gt;CompanyStatsJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;perform_later&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;notice: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Generating statistics...&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# == Schema Information&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Table name: job_statuses&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  id            :bigint           not null, primary key&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  completed     :boolean          default(FALSE), not null&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  download_key  :string&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  error         :boolean          default(FALSE), not null&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  error_message :text&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  log           :text             default(&quot;&quot;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  name          :string           not null&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  created_at    :datetime         not null&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  updated_at    :datetime         not null&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JobStatus&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;add_progress!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;update!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;log: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;==&amp;gt; &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mark_completed!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;update!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;completed: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;download_key: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;log: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Report complete!&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;mark_failed!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;update!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;error: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;error_message: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;log_error!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;update!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;log: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;ERROR: &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CompanyStatsJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationJob&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_progress!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Generating stats for &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;report&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;CompanyStatsReport&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;company&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Do some complicated calculations&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_progress!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Computing financial data...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_financial_data!&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_progress!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Computing employee data...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_employee_data!&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_progress!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Computing product data...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_product_data!&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Store the report on a file system for downloading&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;file_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AwsService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;upload_s3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;as_xlsx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;add_progress!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Report completed.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mark_completed!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;StandardError&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mark_failed!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;log_error!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JobStatusController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;show&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;JobStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Return a basic HTML page and a JSON version for polling&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;respond_to&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;html&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;json: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@status&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- application/views/job_status/show.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;job-status&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-job-status-url=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;job_status_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;format: :json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;job-status.log&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Download&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;#&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;btn hidden&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;target: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;job-status.download&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/controllers/job_status_controller.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Controller&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;targets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;download&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Start polling at 1 second interval&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;refresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;refresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// Update log and auto-scroll to the bottom&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;logTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;innerText&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;logTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scrollTop&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;logTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scrollHeight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stopRefresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;// Add additional error handling as needed&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;completed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stopRefresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

          &lt;span class=&quot;c1&quot;&gt;// Show a download button, or take some other action&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;downloadTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;href&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;`/download/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;download_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;downloadTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;classList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;hidden&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;stopRefresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;disconnect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Stop the timer when we teardown the component&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stopRefresh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Aside: there are tools like &lt;a href=&quot;https://github.com/utgarda/sidekiq-status&quot;&gt;sidekiq-status&lt;/a&gt; that you can use to track progress instead if you want more features or a different API. One thing you should not do is to try adding extra status information directly to the underlying &lt;code class=&quot;highlighter-rouge&quot;&gt;jobs&lt;/code&gt; table as most processors will clear out the records once the job has been processed (leaving you unable to query the status).&lt;/p&gt;

&lt;h3 id=&quot;mix-match-and-modify&quot;&gt;Mix, Match, and Modify&lt;/h3&gt;

&lt;p&gt;These two approaches should serve as solid ways to tackle the problem, but feel free to combine or modify them to suit your own needs.&lt;/p&gt;

&lt;p&gt;Maybe you want to add a flag that the file was downloaded and send an email later if the user closed the tab while waiting for it to finish. Maybe you want to redirect the user to a new page in the app instead of downloading a file. Maybe you just need a loading spinner or basic progress bar.&lt;/p&gt;

&lt;p class=&quot;pro-tip&quot;&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Background processing is a language-agnostic concept, so don’t be shy about learning from other development ecosystems&lt;/p&gt;

&lt;p&gt;Think of these patterns as good building blocks and then layer on additional complexity as needed. You might want to clean up the &lt;code class=&quot;highlighter-rouge&quot;&gt;job_statuses&lt;/code&gt; table after a few days or add more robust authentication or use the job status as a way to cache results. It’s up to you.&lt;/p&gt;

&lt;p&gt;This code should serve as a conceptual guideline and should be easy to extend and grow as your needs change.&lt;/p&gt;

&lt;h2 id=&quot;advanced-techniques-proceed-with-caution&quot;&gt;Advanced Techniques: Proceed with Caution&lt;/h2&gt;

&lt;p&gt;It is outside the scope of this article to cover more advanced techniques, but if you find yourself needing something even more powerful the next step would be to implement some kind of &lt;a href=&quot;https://medium.com/@tdaniel/using-actioncable-to-provide-updates-on-background-job-in-your-rails-app-29b6fd41ed70&quot;&gt;ActionCable or web-socket approach&lt;/a&gt;, maybe combined with &lt;a href=&quot;https://evilmartians.com/chronicles/crafting-user-notifications-in-rails-with-active-delivery&quot;&gt;in-app or push notifications&lt;/a&gt;. Most applications will never need to go this far.&lt;/p&gt;

&lt;p&gt;If you have reports that are easy to pre-compute and store – for instance, a monthly account statement – you could switch from “on-demand” report generation to &lt;a href=&quot;https://dev.to/risafj/cron-jobs-in-rails-a-simple-guide-to-actually-using-the-whenever-gem-now-with-tasks-2omi&quot;&gt;cron/scheduled generation&lt;/a&gt;. If the report contents are unlikely to change after a certain date, it is a good candidate for pre-computing.&lt;/p&gt;

&lt;h2 id=&quot;wrap-it-up&quot;&gt;Wrap It Up&lt;/h2&gt;

&lt;p&gt;You will reach a point where you cannot solve every performance problem by optimizing queries. Long-running reports or large file exports should be moved into a background job for processing outside the life cycle of a single HTTP request.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_job_basics.html&quot;&gt;Active Job&lt;/a&gt; framework in Rails provides a solid, standardized interface for enqueueing work. There are battle-tested, community-supported tools – Sidekiq and Delayed Job – that can crank through thousands of jobs a day without any hiccups.&lt;/p&gt;

&lt;p&gt;Follow these basic (but robust!) patterns for displaying progress and sending data back to the user once it’s been processed. There is a modest up-front investment to get your first background job up and running, but it will payoff immediately as your app grows.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Sometimes we need to generate really large file exports or run reports that are just slow. It&apos;s not enough to optimize a few queries, we need to move the work to a background job and notify the user when it&apos;s all done.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/background-jobs.png" />
          <media:content medium="image" url="https://boringrails.com/images/background-jobs.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
    
    

    
      <entry>
        

        <title type="html">Managing Rails schema and data migrations without losing your mind</title>
        <link href="https://boringrails.com/articles/rails-database-migrations-strategy-how-to-manage-migrations-without-losing-your-mind/" rel="alternate" type="text/html" title="Managing Rails schema and data migrations without losing your mind" />
        <published>2019-11-05T13:00:00+00:00</published>
        <updated>2019-11-05T13:00:00+00:00</updated>
        <id>https://boringrails.com/articles/rails-database-migrations-strategy-how-to-manage-migrations-without-losing-your-mind</id>
        
        
          <content type="html" xml:base="https://boringrails.com/articles/rails-database-migrations-strategy-how-to-manage-migrations-without-losing-your-mind/">&lt;p&gt;As time goes by, you’ll need to change the data model for your software. Rails makes this extremely easy to do, but leaves much of the day-to-day process to you to sort out (the principle of &lt;a href=&quot;https://rubyonrails.org/doctrine/#provide-sharp-knives&quot;&gt;Sharp Knifes&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Once you’re past the initial scaffolding stage of a project, you’ll have questions about how to manage your application schema, especially when working on a team.&lt;/p&gt;

&lt;p&gt;Adding rules to your process codifies good practices; a boring checklist isn’t sexy, but it will get the job done and free you up for more valuable work.&lt;/p&gt;

&lt;p&gt;Unless you have strong, context-specific reasons not to, follow these rules:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use migrations only for schema changes&lt;/li&gt;
  &lt;li&gt;Use one-off scripts to seed/import data&lt;/li&gt;
  &lt;li&gt;Aggressively prune scripts and sync environments&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;migrations-for-only-schema-changes&quot;&gt;Migrations for only schema changes&lt;/h2&gt;

&lt;p&gt;The &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_record_migrations.html&quot;&gt;ActiveRecord Migration DSL&lt;/a&gt; is one of the nicest toolkits in all of web programming: it’s easy to write migrations, test them locally, and the design of how versions are handled is very robust.&lt;/p&gt;

&lt;p&gt;Since migration files are Ruby classes, you can put any kind of arbitrary code in them. You could use the migrations DSL, execute raw SQL commands, or create records using your application code.&lt;/p&gt;

&lt;p&gt;But you will find plenty of advice that you should NOT reference ActiveRecord models during migrations.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Over the life of your Ruby on Rails application, your app’s models will change dramatically, but according to the Rails guides, your migrations shouldn’t:&lt;/p&gt;

  &lt;p&gt;“In general, editing existing migrations is not a good idea. You will be creating extra work for yourself and your co-workers and cause major headaches if the existing version of the migration has already been run on production machines. Instead, you should write a new migration that performs the changes you require.”&lt;/p&gt;

  &lt;p&gt;That means that if your migrations reference the ActiveRecord model objects you’ve defined in app/models, your old migrations are likely to break. That’s not good.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This advice comes from the README for &lt;code class=&quot;highlighter-rouge&quot;&gt;good-migrations&lt;/code&gt; – &lt;a href=&quot;https://github.com/testdouble/good-migrations&quot;&gt;a gem that prevents loading your models in migrations&lt;/a&gt; so it becomes impossible to get yourself into a mess. If you can’t load your models, you can’t accidentally reference them in migrations.&lt;/p&gt;

&lt;p&gt;As your codebase evolves, &lt;code class=&quot;highlighter-rouge&quot;&gt;Model.create&lt;/code&gt; or &lt;code class=&quot;highlighter-rouge&quot;&gt;Model.update&lt;/code&gt; calls sprinkled throughout old migrations makes it hard to run the migrations against a completely new database without errors.&lt;/p&gt;

&lt;p&gt;You can work around this by redefining models inside the scope of your migration or be being very diligent about refactoring old migrations if you make breaking changes, but the bottom-line is: it’s not worth it.&lt;/p&gt;

&lt;p&gt;Getting your migrations into a broken state is a huge mess – everyone on your team might end up with a slightly different local database or your environments get out of sync based on what order migrations have been run. If you don’t do it correctly, your CI server won’t be able to rebuild a test database from scratch.&lt;/p&gt;

&lt;p&gt;Fix the problem by avoiding it all together: &lt;em&gt;only do schema migrations&lt;/em&gt;. If you see code other than the migrations DSL in a migration, don’t merge it!&lt;/p&gt;

&lt;h2 id=&quot;one-off-scripts-for-seedingimporting-data&quot;&gt;One-off scripts for seeding/importing data&lt;/h2&gt;

&lt;p&gt;In a data-heavy application, it’s common to import or modify a bunch of data right after a schema change. For this situation, write one-off scripts and execute them with &lt;code class=&quot;highlighter-rouge&quot;&gt;rails runner&lt;/code&gt;. I recommend putting these scripts in a &lt;code class=&quot;highlighter-rouge&quot;&gt;db/script&lt;/code&gt; folder.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Write a script to create the data you need (seed, import from file, bulk update, etc)&lt;/li&gt;
  &lt;li&gt;Test it against your local database&lt;/li&gt;
  &lt;li&gt;Deploy to your other environments and run the script&lt;/li&gt;
  &lt;li&gt;Remove the script from source control once it’s been run everywhere (more on this in the next section)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s an example:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AddArticleCategories&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;6.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;change&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;create_table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:article_categories&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;unique: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Seed initial categories&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rails&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ArticleCategory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;js&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ArticleCategory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;javascript&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ArticleCategory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;git&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Back-fill categories&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Article&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;downcase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;includes?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rails&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;category: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Done!&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Run locally&lt;/span&gt;
bin/rails runner db/script/seed_categories.rb

&lt;span class=&quot;c&quot;&gt;# Push script to staging and run migration&lt;/span&gt;
heroku run &lt;span class=&quot;nt&quot;&gt;--app&lt;/span&gt; my-app-test bin/rails runner db/script/seed_categories.rb

&lt;span class=&quot;c&quot;&gt;# Push script to production and run migration&lt;/span&gt;
heroku run &lt;span class=&quot;nt&quot;&gt;--app&lt;/span&gt; my-app bin/rails runner db/script/seed_categories.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now that the script has been run on production, you can delete it from the project.&lt;/p&gt;

&lt;h2 id=&quot;aggressively-prune-and-sync-environments&quot;&gt;Aggressively prune and sync environments&lt;/h2&gt;

&lt;p&gt;With this approach, we treat &lt;strong&gt;schema migrations&lt;/strong&gt; as durable and &lt;strong&gt;data scripts&lt;/strong&gt; as disposable. Once the script has been run on production, it doesn’t need to exist anymore.&lt;/p&gt;

&lt;p&gt;If you need to reference it again, use your &lt;code class=&quot;highlighter-rouge&quot;&gt;git&lt;/code&gt; history, not a huge folder full of potentially out-dated and broken scripts. And in some cases, it is harmful to keep around old scripts that someone might accidentally re-runs a few weeks later.&lt;/p&gt;

&lt;p&gt;But what if someone on your team was out sick and didn’t get the script run locally and now it’s already been removed? Start treating your database environments like a one-way street.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Production is the gold standard: whatever is on production is the absolute source of truth&lt;/li&gt;
  &lt;li&gt;You can pull down production to a staging/test environment&lt;/li&gt;
  &lt;li&gt;You can pull down staging to local development databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data can safely flow from Production -&amp;gt; Staging -&amp;gt; Development but never the other way.&lt;/p&gt;

&lt;p&gt;If someone needs to get up-to-date, have them clone the staging database to their local database. Periodically clone the production database to the staging environment as part of your release process.&lt;/p&gt;

&lt;p&gt;If you’re on Heroku, use &lt;code class=&quot;highlighter-rouge&quot;&gt;parity&lt;/code&gt; to do this. It’s a super convenient way to add &lt;a href=&quot;https://github.com/thoughtbot/parity&quot;&gt;one-line “copy this environment to that environment” functionality&lt;/a&gt; to your project.&lt;/p&gt;

&lt;h2 id=&quot;optional-clean-up-old-migrations&quot;&gt;Optional: clean up old migrations&lt;/h2&gt;

&lt;p&gt;Depending on how complex your application is, you may find yourself drowning in migration files. There is no harm in having them in your &lt;code class=&quot;highlighter-rouge&quot;&gt;db/migrations&lt;/code&gt; folder, but if you want, you can delete them.&lt;/p&gt;

&lt;p&gt;Just make sure you switch from a &lt;code class=&quot;highlighter-rouge&quot;&gt;db:create&lt;/code&gt;/&lt;code class=&quot;highlighter-rouge&quot;&gt;db:migrate&lt;/code&gt; approach to a &lt;code class=&quot;highlighter-rouge&quot;&gt;db:schema:load&lt;/code&gt; approach on CI/test.&lt;/p&gt;

&lt;p&gt;As with data scripts, if you need to refer back to some old migrations you have source control for that.&lt;/p&gt;

&lt;h2 id=&quot;when-to-deviate&quot;&gt;When to deviate&lt;/h2&gt;

&lt;p&gt;You’ll need to reach for a different workflow if:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You need zero downtime migrations&lt;/li&gt;
  &lt;li&gt;You have sensitive data that prevents syncing environments&lt;/li&gt;
  &lt;li&gt;You have have a complicated multiple database setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…and probably several other circumstances. As they say: “but of course there are obvious exceptions…”&lt;/p&gt;

&lt;p&gt;If this is the case, you should deviate from these rules!&lt;/p&gt;

&lt;p&gt;But think really hard and challenge if you &lt;em&gt;actually&lt;/em&gt; have those requirements before adding extra complexity to your process. You can always add capabilities later as needed, but it’s much harder to take them away once you become dependant on them.&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Especially when it comes to touching production data, &lt;a href=&quot;https://boringrails.com&quot;&gt;being boring is a virtue&lt;/a&gt;. We don’t want fancy setups because if (let’s be honest: when…) they break, we are entering a high stress situation.&lt;/p&gt;

&lt;p&gt;Imagine you are trying to do a deploy and the migrations fail – or the schema changes work, but there is an exception seeding some data. Now you have a problem. Do you rollback? SSH in and re-run the script? Start flailing around in the Rails console? Break out into a cold sweat?&lt;/p&gt;

&lt;p&gt;If you’re ever spent an hour trying to help diagnosis why a teammate’s local database isn’t matching your own or why the &lt;code class=&quot;highlighter-rouge&quot;&gt;schema.rb&lt;/code&gt; file seems to change every time you run migrations, you would benefit from this strategy.&lt;/p&gt;

&lt;p&gt;If you follow these rules:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Use migrations only for schema changes&lt;/li&gt;
  &lt;li&gt;Use one-off scripts to seed/import data&lt;/li&gt;
  &lt;li&gt;Aggressively prune scripts and sync environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll be well positioned for reliable database migrations with clear guardrails around how you (and your team) should approach changing data.&lt;/p&gt;</content>
        

        
        
        
        
        

        <author>
            <name>Matt Swanson</name>
          
          
        </author>

        
          
            <category term="post" />
          
        

        

        
          <summary type="html">Rails database migrations are extremely powerful, but can be a mess if we don&apos;t avoid the traps. This article outlines a boring way to handle schema and data migrations effectively.</summary>
        

        
        
          
          <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://boringrails.com/images/database.png" />
          <media:content medium="image" url="https://boringrails.com/images/database.png" xmlns:media="http://search.yahoo.com/mrss/" />
        
      </entry>
    
  
</feed>
