<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>Colby.so</title>
 <link href="https://colby.so/atom.xml" rel="self"/>
 <link href="https://colby.so/"/>
 <updated>2026-08-16T15:10:55+00:00</updated>
 <id>https://colby.so</id>
 <author>
   <name>David Colby</name>
   <email>david@colby.so</email>
 </author>

 
 <entry>
   <title>Searching and filtering with Turbo 8</title>
   <link href="https://colby.so/posts/turbo-8-search-and-filter"/>
   <updated>2024-03-27T00:00:00+00:00</updated>
   <id>https://colby.so/posts/turbo-8-search-and-filter</id>
   <content type="html">&lt;p&gt;Today we are going to build on top of &lt;a href=&quot;https://www.colby.so/posts/turbo-8-refresh-sorting&quot;&gt;an article I published last week&lt;/a&gt; to expand our knowledge of Turbo 8’s page refreshes and, as a treat, we will write some beautiful Ruby code along the way.&lt;/p&gt;

&lt;p&gt;In this article we are going to add searching and filtering to an existing sortable table interface. We will use Turbo 8 page refreshing, a tiny Stimulus controller, and regular old Rails code to build a solution that maintains the user’s page state and scroll position while they search and filter the results shown in a table, no Turbo Frames or Streams required.&lt;/p&gt;

&lt;p&gt;When we are finished, the project will look like this:&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-search-and-filter-4.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;This article assumes you are comfortable writing Rails code. You do not need any prior experience with Turbo or Stimulus to follow along. Reading the &lt;a href=&quot;https://www.colby.so/posts/turbo-8-refresh-sorting&quot;&gt;previous article&lt;/a&gt; first will help clarify some of the concepts that we move over quickly in this article. You really should read the previous article before you read this one, but I’m not the boss, live life on the edge if you want.&lt;/p&gt;

&lt;p&gt;When you’re ready, dive in.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;

&lt;p&gt;This article picks up where the previous article left off and you can pick up your code where you left it at the end of that article or clone &lt;a href=&quot;https://github.com/DavidColby/turbo_8_refresh_sorting&quot;&gt;this GitHub repo&lt;/a&gt; as a starting point.&lt;/p&gt;

&lt;p&gt;If you choose to clone the repo instead of working from the previous project, you will need to work from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;finished-project&lt;/code&gt; branch of that repo. After you clone the repo to your local machine, start in your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;turbo_8_refresh_sorting
git fetch origin finished-project
git checkout finished-project
bin/setup&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If you did not follow along with the previous article, you will also need to populate some data in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; table. Run this simple script from the Rails console:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Dallas Mavericks&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Denver Nuggets&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Phoenix Suns&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;San Antonio Spurs&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Boston Celtics&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Miami Heat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Orleans Pelicans&apos;&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;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Team&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;nb&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;mi&quot;&gt;100&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;no&quot;&gt;Player&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;no&quot;&gt;Faker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Name&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;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Team&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;no&quot;&gt;Team&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;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sample&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;seasons: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&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;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, whether you followed along with the previous article or not, head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; and update it to fix an issue that causes the table to resize in a jarring way when the list of players changes:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_refreshes_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :morph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;scroll: :preserve&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;max-w-7xl mx-auto mt-12 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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;shadow rounded border-b border-gray-200&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;bg-white 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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;show_sort_indicator_for&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;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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &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;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
          &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;show_sort_indicator_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
          &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;show_sort_indicator_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Seasons&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;/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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;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;vi&quot;&gt;@players&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;player&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&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 w-1/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;n&quot;&gt;player&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;/td&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 w-1/4&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;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;team&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;/td&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 w-1/4&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;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seasons&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;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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we set fixed widths on the table columns and made the table wider by default. These changes are not core to the function of the project, but they make it nicer to look at as we work.&lt;/p&gt;

&lt;p&gt;If all has gone well you can start the rails server with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; and go to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt; and see that you have a list of players that can be sorted by clicking on the column headers.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-search-and-filter-1.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;h2 id=&quot;searching&quot;&gt;Searching&lt;/h2&gt;

&lt;p&gt;Our goal is to be able to search players by name and to filter the list of players by their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Team&lt;/code&gt; while maintaining any existing sorting applied to the table.&lt;/p&gt;

&lt;p&gt;We will take it one step at a time, starting by adding a simple search form to the players index that queries the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; table by player name. Add the search form to the players index at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt;, just above the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;table&amp;gt;&lt;/code&gt; element:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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-end mb-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;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;n&quot;&gt;params&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;border border-blue-500 rounded p-2&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search&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;bg-blue-500 text-white p-2 rounded-sm&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This is a standard Rails form. Type in a name, click search, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt; request fires off to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players&lt;/code&gt;. We will make it fancier soon, but first let’s make it work. This form submission will just reload the index page without changing the list of players because we are not doing anything with the form submission on the server. Let’s change that now.&lt;/p&gt;

&lt;p&gt;Go to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players/players_controller.rb&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;players.name like ?&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;params&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;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;if&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;:name&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;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;direction&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;values&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;s1&quot;&gt;&apos; &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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This code is clunky, but it gets the job done. It will be beautiful Ruby code before we finish this article, but it works and working is good enough for now.&lt;/p&gt;

&lt;p&gt;Try another search on the players index page and you will see that the list of players filters down to match your search term. We are not using Turbo yet, but we have a working search form. Before we add page refreshing, clicking to submit a search form is very 2010s, results should update as the user types, right?&lt;/p&gt;

&lt;p&gt;Let’s solve that next with a small Stimulus controller.&lt;/p&gt;

&lt;h2 id=&quot;automatically-submit-the-search-form&quot;&gt;Automatically submit the search form&lt;/h2&gt;

&lt;p&gt;The Stimulus controller’s job will be to listen to changes to the form inputs (for now just the name field) and submit the form in response to those changes. This allows us to build a type-as-you-search experience instead of asking the user to click submit and see what comes back.&lt;/p&gt;

&lt;p&gt;Get started by generating a new Stimulus controller from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bin/rails g stimulus autosubmit
bin/rails stimulus:manifest:update&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then fill in the new controller at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/autosubmit_controller.js&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;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;nc&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;nf&quot;&gt;submit&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;clearTimeout&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;timeout&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;timeout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTimeout&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;requestSubmit&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;250&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The important line in this controller is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.element.requestSubmit()&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.element&lt;/code&gt; is a reference to the DOM element that the controller is attached to. In this project, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.element&lt;/code&gt; will be the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; we added in the last section. Calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit&lt;/code&gt; on the form element, naturally, submits it.&lt;/p&gt;

&lt;p&gt;A very important note here is that we are calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit&lt;/code&gt; on the form element, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit&lt;/code&gt; is an entirely different thing that submits the form but does not trigger the form’s submit event. Turbo &lt;a href=&quot;https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/observers/form_submit_observer.js#L11&quot;&gt;listens for&lt;/a&gt; the form’s submit event to process the request, and using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit&lt;/code&gt; will completely bypass Turbo. There is a long, worthwhile discussion on the &lt;a href=&quot;https://discuss.hotwired.dev/t/triggering-turbo-frame-with-js/1622/12&quot;&gt;Hotwire forums&lt;/a&gt; about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit&lt;/code&gt; if you want to get into the weeds.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clearTimeout&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt; calls are a simple implementation of a debounce function so that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit&lt;/code&gt; does not fire every single time the user types a key. Instead, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.element.requestSubmit()&lt;/code&gt; will not fire until 250ms after the user stops typing. This slight delay avoids hammering the server with a request for every keypress without adding a noticeable delay for the user. If debouncing is a new concept for you, this is a &lt;a href=&quot;https://chrisboakes.com/how-a-javascript-debounce-function-works/&quot;&gt;great article&lt;/a&gt; on the topic.&lt;/p&gt;

&lt;p&gt;Now that we have our Stimulus controller, we need to attach it to the search form. Update the form in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;autosubmit&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;n&quot;&gt;params&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;border border-blue-500 rounded p-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;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;autosubmit#submit&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;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller&lt;/code&gt; attribute to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element and added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action=&quot;autosubmit#submit&quot;&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text_field&lt;/code&gt;. In Stimulus, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; elements &lt;a href=&quot;https://stimulus.hotwired.dev/reference/actions#event-shorthand&quot;&gt;assume&lt;/a&gt; that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;action&lt;/code&gt; to fire on is an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event&quot;&gt;input event&lt;/a&gt;, so you can leave the event out of the &lt;a href=&quot;https://stimulus.hotwired.dev/reference/actions#descriptors&quot;&gt;action descriptor&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action=&quot;input-&amp;gt;autosubmit#submit&quot;&lt;/code&gt; would work fine, if you really like being explicit.&lt;/p&gt;

&lt;p&gt;Since the form submits as the user types, we also removed the unnecessary submit button. This means that users with JavaScript disabled will not be able to submit a form, but I am going to assume you trust me enough to enable JavaScript as you follow along with this tutorial.&lt;/p&gt;

&lt;p&gt;Now that we have the Stimulus controller connected to the form, refresh the players index page and try out the new search experience.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-search-and-filter-2.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Oh. That’s not what we wanted. Type-as-you-search is not very useful if we lose focus each time the search form submits!&lt;/p&gt;

&lt;p&gt;What’s going on here? Right now, the search form is a standard Rails form. Each time we submit the form, a full page load occurs. The page content is replaced, scroll position is lost, and ephemeral state (like the user’s current focus) is lost.&lt;/p&gt;

&lt;p&gt;This is where Turbo 8’s morphing page refreshes come in. Instead of a normal page turn, we can use morphing to replace only the content that’s changed, leaving page state and unchanged content untouched. Just like in the previous article, we will write very little code to make this happen.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; and update the form again:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;replace&quot;&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;autosubmit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_permanent: &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;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;border border-blue-500 rounded p-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;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;autosubmit#submit&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;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added two new data attributes to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-action=&quot;replace&quot;&lt;/code&gt; tells Turbo to treat these form submissions as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; visits instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt; visits. This change enables morphing refresh behavior for our form — as discussed in detail in the previous article, without setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-action&lt;/code&gt; on the visit, page refreshing will not be used.&lt;/p&gt;

&lt;p&gt;As mentioned in the previous article, the use of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; instead of the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt; visit means that submitting the search form will not add a new entry into the user’s browser history, so they will not be able to use the back button to move between different searches. When a form is submitted as the user types, not preserving history for each search is probably the best call, but it is important to note that if preserving each form submission in the history is important, morphing page refreshes are not the right tool.&lt;/p&gt;

&lt;p&gt;The other new data attribute is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-permanent=&quot;&quot;&lt;/code&gt;. We use this &lt;a href=&quot;https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads&quot;&gt;attribute&lt;/a&gt; to inform Turbo that the element (and the element’s children) should not be morphed on refresh visits. Form elements without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-permanent&lt;/code&gt; will lose focus and be reset to their original (empty) state each time new content is morphed in during a refresh.&lt;/p&gt;

&lt;p&gt;With both data attributes in place (and &lt;a href=&quot;https://turbo.hotwired.dev/reference/attributes#meta-tags&quot;&gt;the turbo-refresh meta tags&lt;/a&gt; we added in the previous article), we have everything we need for our live search form to work as expected. Refresh the page and see that as you type, the list of players is updated without losing focus.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-search-and-filter-3.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Great work so far!&lt;/p&gt;

&lt;p&gt;We have written all of the Turbo 8 code we need to write. Two data attributes, two meta tags, and some standard Rails code to deliver search-as-you-type is a delight. In previous versions of Turbo we could achieve the same experience, but we needed to use Turbo Frames, which bring more complexity and a learning curve. We can stay closer to standard Rails, simplify our code, and still deliver a great user experience with page refreshes.&lt;/p&gt;

&lt;p&gt;Let’s continue this article by improving the existing search and sort functionality and adding the ability to filter the table by team.&lt;/p&gt;

&lt;h2 id=&quot;search-and-sort-together&quot;&gt;Search and sort together&lt;/h2&gt;
&lt;p&gt;As you tested out the code on your machine you may have noticed a problem with our search and sort functionality. You cannot use both at the same time.&lt;/p&gt;

&lt;p&gt;When you apply a search, the sort order is removed, and when you apply a sort order, any search is cleared away. This happens because we are using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; to query and sort &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@players&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; index action, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;players.name LIKE ?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%{params[:name]}%&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;params&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;nf&quot;&gt;present?&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@players&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;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;direction&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;values&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;s1&quot;&gt;&apos; &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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The search form does not submit with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; params, and the links to sort in the table headers do not include params from the search form and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; are not permanent, if they are not present in the current request, they do not exist.&lt;/p&gt;

&lt;p&gt;There are a few ways to handle this situation. We could try to keep the current &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; in sync, making sure the search form always included existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; params in the form submission and pushing the current state of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; params into each sort link, but that is a lot of work.&lt;/p&gt;

&lt;p&gt;Instead, we will move from using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; to query for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; to storing search and sort values in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt;. Storing search and filter data in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt; allows us to persist that data across requests, and is a great solution to support more complex sorting and filtering UIs like ours.&lt;/p&gt;

&lt;p&gt;To move from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt;, first update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&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;p&quot;&gt;{}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;merge!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;players.name LIKE ?&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;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&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;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&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;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;direction&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;values&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;s1&quot;&gt;&apos; &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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The queries are the same, but instead of using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; for values, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;filters&apos;]&lt;/code&gt;. Before querying for the players we set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;filters&apos;]&lt;/code&gt; based on any new inbound &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge!(filter_params)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_params&lt;/code&gt; is a method that we have not defined yet. Define it next, staying in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;kp&quot;&gt;private&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Snip existing private methods&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filter_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;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;:column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:direction&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_params&lt;/code&gt; to whitelist the attributes that can be used for filtering, any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;param&lt;/code&gt; not in this list will be discarded instead of being stored in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With these changes in place, sorting and searching at the same time will work, but the sort indicators will not work as expected. This is because the indicators rely on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt; values. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt; to fix that:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PlayersHelper&lt;/span&gt;  
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;direction&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;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&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;n&quot;&gt;next_direction&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&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;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;direction: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;direction&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_action: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;replace&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;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;next_direction&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;direction&apos;&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;s1&quot;&gt;&apos;asc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;desc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&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;sort_indicator&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;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;sort sort-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;direction&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;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;show_sort_indicator_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&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;column&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we replaced references to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;filters&apos;]&lt;/code&gt;, everything else is the same.&lt;/p&gt;

&lt;p&gt;Finally, we need to read the value of the search field from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;filters&apos;]&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; to, which we can do by updating the form in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;replace&quot;&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;autosubmit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_permanent: &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;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&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;border border-blue-500 rounded p-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;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;autosubmit#submit&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;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dig&lt;/code&gt; for the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;filters&apos;][&apos;name&apos;]&lt;/code&gt; and use it to set the value of the search field, plain old Ruby here.&lt;/p&gt;

&lt;p&gt;Now reload &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt; and see that you can search by name and apply sorting in both directions and everything works as expected.&lt;/p&gt;

&lt;p&gt;Next up, we are going to add the ability to filter player’s by their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Team&lt;/code&gt;. In the process, we will clean up the query logic in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method. Let’s dig in.&lt;/p&gt;

&lt;h2 id=&quot;filter-by-team&quot;&gt;Filter by team&lt;/h2&gt;

&lt;p&gt;Before we add a new filter for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Team&lt;/code&gt;, we should address the clunky code in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController#index&lt;/code&gt;. Each new filter we add means another conditional in the method, and it is already hard to understand at a glance.&lt;/p&gt;

&lt;p&gt;Fortunately, we can make this code easier to read and maintain with a few small adjustments. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&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;p&quot;&gt;{}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;merge!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;then&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;search_by_name&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_1&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;then&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;apply_order&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_1&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then define the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search_by_name&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apply_order&lt;/code&gt; methods, staying in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;kp&quot;&gt;private&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Snip existing private methods&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;search_by_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&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;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scope&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;players.name like ?&apos;&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;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&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;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scope&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;apply_order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;scope&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;direction&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;values&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;s1&quot;&gt;&apos; &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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Let’s pause here and look at what might be some surprising syntax. In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method we are using &lt;a href=&quot;https://docs.ruby-lang.org/en/3.1/Kernel.html#method-i-then&quot;&gt;then&lt;/a&gt; and &lt;a href=&quot;https://docs.ruby-lang.org/en/3.1/Proc.html#class-Proc-label-Numbered+parameters&quot;&gt;numbered parameters&lt;/a&gt;, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;search_by_name&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;then&lt;/code&gt; is an alias for &lt;a href=&quot;https://docs.ruby-lang.org/en/3.1/Kernel.html#method-i-yield_self&quot;&gt;yield_self&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;then&lt;/code&gt; (and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield_self&lt;/code&gt;) yields self to a block and then returns the result of the block, and is helpful for exactly our use case; passing an object through a chain of method calls.&lt;/p&gt;

&lt;p&gt;Numbered parameters (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_1&lt;/code&gt;) were introduced in Ruby 2.7 to simplify writing short blocks by allowing us to skip naming the parameters passed to the block. Alternatively, we could write the same line as:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;yield_self&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;scope&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;search_by_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scope&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;But why write Ruby if we don’t care about aesthetics?&lt;/p&gt;

&lt;p&gt;The use of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;then&lt;/code&gt; with numbered parameters in this article came from these excellent articles from the folks at ThoughtBot:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://thoughtbot.com/blog/using-yieldself-for-composable-activerecord-relations&quot;&gt;Using yield_self for composable ActiveRecord relations&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://thoughtbot.com/blog/pipelining-without-pipes-in-ruby&quot;&gt;Pipelining without pipelines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our refactored &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method still does the same thing, but it is now easier to scan the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method to understand what’s going on. The refactor also gives us a pattern we can use to support filtering by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Team&lt;/code&gt;. Let’s do that now, staying in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&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;p&quot;&gt;{}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;merge!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;then&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;search_by_name&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_1&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;then&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filter_by_team&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_1&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;then&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;apply_order&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_1&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;One new line here, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.then { filter_by_team _1 }&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_by_team&lt;/code&gt; is not defined yet, define it next in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; private methods:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filter_by_team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;team_id&apos;&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;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;scope&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;team_id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;team_id&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;n&quot;&gt;scope&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_params&lt;/code&gt; to whitelist the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;team_id&lt;/code&gt; attribute:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filter_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;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;:team_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:direction&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now that we support filtering by team in the controller, we can update the form in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;replace&quot;&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;autosubmit&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_permanent: &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;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&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;ss&quot;&gt;:team_id&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;no&quot;&gt;Team&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;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;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;team_id&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;span class=&quot;ss&quot;&gt;include_blank: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;All Teams&apos;&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;border-blue-500 rounded&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;autosubmit#submit&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&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;border border-blue-500 rounded p-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;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;autosubmit#submit&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;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;team_id&lt;/code&gt; select that displays all of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Team&lt;/code&gt; records in the database. Note the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; attribute that submits the form automatically.&lt;/p&gt;

&lt;p&gt;Load up &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt; one last time and see the new Team filter option. Test it out and see that you can apply any combination of sorting, searching, and filtering and it will all work as expected. With a couple lines of Turbo, and some regular old Rails and Ruby code we now have an interactive table UI that feels great to use and will look familiar to any Rails developer, even if they are new to Turbo.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-search-and-filter-4.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;That’s all for this article, great job following along and thank you for reading! As always, you can find the full code for this article &lt;a href=&quot;https://github.com/DavidColby/turbo_8_refresh_sorting/tree/sort-and-filter&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;Today we enhanced our sortable table by adding live searching and filtering, powered by a little bit of Stimulus and a splash of Turbo.&lt;/p&gt;

&lt;p&gt;Turbo 8 plus Rails gives us the tools to write simple, Rails-y code without sacrificing user experience. To me, Turbo 8 feels peaceful in a way that previous versions of Turbo did not.&lt;/p&gt;

&lt;p&gt;Peaceful is an odd term, maybe, to describe writing code, but that’s what it feels like after a few years of watching Turbo evolve, and seeing folks struggle with the conceptual shifts required to use Turbo. As we saw in this article and the first article in this series, page refreshes are vanilla Rails with a dash of magic on top. Things work like you expect them to in a way that feels, to me, peaceful, calm, and a tiny bit magical.&lt;/p&gt;

&lt;p&gt;If you want to dive deeper into the topics we covered today:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The articles discussing uses for &lt;a href=&quot;https://thoughtbot.com/blog/using-yieldself-for-composable-activerecord-relations&quot;&gt;yield_self&lt;/a&gt; and &lt;a href=&quot;https://thoughtbot.com/blog/pipelining-without-pipes-in-ruby&quot;&gt;then&lt;/a&gt; that I linked above are really good reads, even if they don’t have anything to do with Hotwire directly&lt;/li&gt;
  &lt;li&gt;In the &lt;a href=&quot;https://www.colby.so/posts/turbo-8-search-and-filter&quot;&gt;previous article&lt;/a&gt; in this series, I linked to a &lt;a href=&quot;https://github.com/hotwired/turbo-site/pull/178&quot;&gt;Github PR&lt;/a&gt; with an important discussion about the difference between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; visits and how they relate to page refreshes.&lt;/li&gt;
  &lt;li&gt;An article always inspires me, not related to Turbo directly is “&lt;a href=&quot;https://dev.37signals.com/vanilla-rails-is-plenty/&quot;&gt;Vanilla Rails is enough&lt;/a&gt;” by Jorge Manrubia. When I think about simple, peaceful Rails code, I think about vanilla Rails.&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a sortable table with Turbo 8's page refreshes</title>
   <link href="https://colby.so/posts/turbo-8-refresh-sorting"/>
   <updated>2024-03-21T00:00:00+00:00</updated>
   <id>https://colby.so/posts/turbo-8-refresh-sorting</id>
   <content type="html">&lt;p&gt;Scroll-restoring, morphing &lt;a href=&quot;https://turbo.hotwired.dev/handbook/page_refreshes&quot;&gt;page refreshes&lt;/a&gt; are one of the headline features of Turbo 8. Page refreshes make it easier than ever to build modern, SPA-like experiences with Turbo and Ruby on Rails.&lt;/p&gt;

&lt;p&gt;One of the most immediate wins for page refreshes is making it simpler to build search and filter UIs, so today we are going to get practical with Turbo 8 by building a sortable table view with page refreshes. I &lt;a href=&quot;https://www.colby.so/posts/sortable-table-with-rails-and-turbo-frames&quot;&gt;wrote about&lt;/a&gt; building sortable tables with Turbo several years ago; back when that article was written we needed to lean heavily on Turbo Frames. With the new tools in Turbo 8 at our disposal, we can simplify our code to the point that we will be writing code that will look familiar to anyone that’s worked in Rails before. Turbo’s page refreshes blend into the background and let us write simple, efficient controllers and views without sacrificing the smooth feel of a single page application.&lt;/p&gt;

&lt;p&gt;The application will allow users to sort a list of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; by their attributes. The players will be displayed in a table, and users will click on the table headers to apply their desired sort order. When we are finished, our project will work like this:&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-refresh-sorting-3.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;To follow along with this article, you should have be comfortable with Ruby on Rails and have Ruby 3 and Rails 7 ready to run locally. You do not need any experience with Turbo, I will fill you in as we work.&lt;/p&gt;

&lt;p&gt;Let’s dive in.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;If you prefer to skip past the setup, you can clone &lt;a href=&quot;https://github.com/DavidColby/turbo_8_refresh_sorting&quot;&gt;this Github repo&lt;/a&gt; and skip ahead to the next section.&lt;/p&gt;

&lt;p&gt;To follow along locally, create a new Rails application with Tailwind added from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new turbo_8_refresh_sorting &lt;span class=&quot;nt&quot;&gt;--css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;tailwind
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;turbo_8_refresh_sorting
bin/setup&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The core of our application is a list of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; that users can sort by name, team, and the number of seasons they played. Staying in your terminal, generate the models and scaffold up a basic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Player&lt;/code&gt; resource to build on. Staying in your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g model Team name:string
rails g scaffold Player name:string team:references seasons:integer
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next use &lt;a href=&quot;https://github.com/faker-ruby/faker&quot;&gt;faker&lt;/a&gt; to generate test data so we can see our sort mechanism working. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle add faker&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then hop into the rails console with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/rails c&lt;/code&gt; and run this code to generate test data in your local database:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Dallas Mavericks&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Denver Nuggets&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Phoenix Suns&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;San Antonio Spurs&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Boston Celtics&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Miami Heat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Orleans Pelicans&apos;&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;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Team&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;nb&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;mi&quot;&gt;100&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;no&quot;&gt;Player&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;no&quot;&gt;Faker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Name&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;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Team&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;no&quot;&gt;Team&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;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sample&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;seasons: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&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;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finish setup by creating a basic table structure on the players index page at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;max-w-7xl mx-auto mt-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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;shadow overflow-hidden rounded border-b border-gray-200&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 bg-white&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Team&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Seasons&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
          &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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;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;vi&quot;&gt;@players&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;player&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&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&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;/td&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;team&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;/td&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seasons&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;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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we are rendering an HTML table with columns for player name, team name, and number of seasons played, and then rendering each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player&lt;/code&gt; in the list of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@players&lt;/code&gt; set by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController#index&lt;/code&gt; action. At this point you can boot up the Rails app with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; and head to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt;. If all has gone well you should a static list of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; that we generated with faker.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/turbo-8-refresh-sorting-1.png&quot; class=&quot;centered&quot; alt=&quot;A screen shot of a website table layout, displaying a list of players with columns for player name, team, and number of seasons played.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;A static table is not very exciting. Next let’s make it possible to sort the table when a user clicks on the column headers.&lt;/p&gt;

&lt;h2 id=&quot;sorting-the-table&quot;&gt;Sorting the table&lt;/h2&gt;
&lt;p&gt;In previous versions of Turbo, the best method for building sorting or filtering features was Turbo Frames. Without too much code or complexity you could get a SPA-like experience that maintained the user’s scroll position and updated the user’s results with data from the server very quickly. Turbo Frames still work fine in Turbo 8 (you do not need to go rip them all out before upgrading), but we can simplify our code with the addition of morphing, scroll preserving page refreshes. With a Turbo-powered refresh, we can write standard Rails code that provides exactly the same experience without any extra views, routes, or controller code.&lt;/p&gt;

&lt;p&gt;First, make the table headers clickable links. When a user clicks on any of the table headers, we will sort the table by that column in ascending order.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;thead&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &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;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Seasons&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;/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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we replaced the text of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;th&amp;gt;&lt;/code&gt; elements in the table with calls to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; method that takes the column to sort by and label text to display in the UI.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; has not been defined yet, do that next in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PlayersHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&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;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Each call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; returns a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt; pointing to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players#index&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;column&lt;/code&gt; in the URL parameters so that we can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;column&lt;/code&gt; from the link to apply the user’s desired sort value to the list of players.&lt;/p&gt;

&lt;p&gt;Head to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;column&lt;/code&gt; param to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt; the list of players:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;k&quot;&gt;if&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;:column&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;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;:column&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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;At this point you can refresh the &lt;a href=&quot;http://localhost:3000/players&quot;&gt;players index page&lt;/a&gt; and see that clicking the header links sorts the table by the column you clicked. But you will notice that each click performs a full page turn, replacing the entire body of the page and resetting the user’s scroll position back to the top of the page. That’s not the experience we are looking for!&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-refresh-sorting-1.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Let’s use a Turbo page refresh, with morphing and scroll preservation to make the sort feel like a seamless, on-page update.&lt;/p&gt;

&lt;p&gt;We need to add &lt;a href=&quot;https://turbo.hotwired.dev/reference/attributes#meta-tags&quot;&gt;two meta tags&lt;/a&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of the players index page to enable Turbo refreshes. These meta tags tell Turbo to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morph&lt;/code&gt; page refreshes with &lt;a href=&quot;https://github.com/bigskysoftware/idiomorph&quot;&gt;idiomorph&lt;/a&gt; and to preserve the user’s scroll position during the refresh.&lt;/p&gt;

&lt;p&gt;The meta tags we need are:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;turbo-refresh-method&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;morph&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;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo-refresh-scroll&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;preserve&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; provides a &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/102a491754d46f7dd924201fcfaf879a0f04b11c/app/helpers/turbo/drive_helper.rb#L70&quot;&gt;handy view helper&lt;/a&gt; we can use to generate both meta tags at once. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; and add the helper:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_refreshes_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :morph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;scroll: :preserve&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head&lt;/code&gt; content&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provided&lt;/code&gt; by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_refreshes_with&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;content_for?&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:head&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;yield&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:head&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;You can add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_refreshes_with&lt;/code&gt; directly into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt; if you prefer:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;head&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_refreshes_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :morph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;scroll: :preserve&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;yield&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:head&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Adding these meta tags only on pages we need them (by calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_refreshes_with&lt;/code&gt; in the players index view) is the safest approach, especially if you are migrating an existing application to Turbo 8 and want to avoid any surprising application behavior with refreshes and scroll preservation in your existing code.&lt;/p&gt;

&lt;p&gt;You can also add the meta tags individually with the dedicated helpers for each tag:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_refresh_method_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:morph&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_refresh_scroll_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:preserve&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Whichever route you choose, once you have the required meta tags in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of the players index page, head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt; to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; helper:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PlayersHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&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;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&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_action: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;replace&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-action&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt;. The addition of this data attribute tells Turbo to clicks on the link as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; visit instead of an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt; visit. There are some subtleties with the implementation of Turbo refreshes when you want a refresh to be triggered by a direct visit to a URL instead of by a redirect initiated by a form submission. It is worth a brief break from coding to talk through these subtleties.&lt;/p&gt;

&lt;p&gt;Turbo only triggers a refresh when a visit is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; visit to the same &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname&quot;&gt;pathname&lt;/a&gt; as the previous page; however, the &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive#application-visits&quot;&gt;default visit type&lt;/a&gt; for a visit in Turbo is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt;. In our case, we are rendering links back to the same URL with different URL parameters to change the sort options. Turbo will treat each of those clicks as an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt; visit and will not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morph&lt;/code&gt; the page. Instead an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt; visit treat will be treated as a normal navigation with the full body replaced and scroll position set to the top of the page.&lt;/p&gt;

&lt;p&gt;That is not the behavior we are looking for, so we override the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-action&lt;/code&gt; to specify a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; visit. Doing this will cause Turbo to morph in the new content and maintain scroll position.&lt;/p&gt;

&lt;p&gt;This can be a bit of a tricky concept. You do not always need to specify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-action&lt;/code&gt; to get refresh behavior. Redirecting after a POST is &lt;a href=&quot;https://github.com/hotwired/turbo/blob/c471974dfaf546c071924935ec74c6c419dbd6ec/src/core/drive/navigator.js#L161-L164&quot;&gt;automatically treated by Turbo&lt;/a&gt; as a replace visit, for example. The intricacies of this can be traced through the code and are best captured in a currently (as of March 2024) &lt;a href=&quot;https://github.com/hotwired/turbo-site/pull/178&quot;&gt;open PR&lt;/a&gt; to the Turbo documentation. It is worth reading through the discussion on this PR to understand the current behavior in more depth.&lt;/p&gt;

&lt;p&gt;It is also important to understand that using a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; visit means that the browser’s back button will not navigate through each change to the sort order. Because a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; visit replaces the last entry in the history stack each time, if you apply a few different sort directions and then click the back button in your browser, you will be taken back to the last page you were on before your initial visit to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players&lt;/code&gt;. This is a tradeoff of using page refreshes in this manner — if maintaining a complete navigation history as sorting options change is important for your use case, you can use Turbo Frames to implement sorting rather than using morphing refreshes. There will be more code to write, but you will get complete browser history as compensation for the extra work.&lt;/p&gt;

&lt;p&gt;Once we have this change in place, we have written all the code we need to implement Turbo refreshing with page morphing and scroll preservation. Reload &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt;, scroll down the page a bit, and then click on the page header and see that the table updates while scroll position is maintained.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-refresh-sorting-2.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Magical.&lt;/p&gt;

&lt;p&gt;Sorting only in one direction is not very useful. Let’s finish up the sortable table implementation by swapping between ascending and descending sort order. No extra Turbo required.&lt;/p&gt;

&lt;h2 id=&quot;sort-both-directions&quot;&gt;Sort both directions&lt;/h2&gt;
&lt;p&gt;First, we will update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; helper to include a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; parameter that toggles between ascending and descending order. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PlayersHelper&lt;/span&gt;  
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;direction&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&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;n&quot;&gt;next_direction&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&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;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;direction: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;direction&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_action: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;replace&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;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;next_direction&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;:direction&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;s1&quot;&gt;&apos;asc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;desc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; parameter to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt;. If the column is currently sorted in ascending order, the next click on that column will sort in descending order, otherwise the next click on the column will sort in ascending order.&lt;/p&gt;

&lt;p&gt;Now head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt; and update it to apply the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; parameter to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt; query:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;:column&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; &lt;/span&gt;&lt;span class=&quot;si&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;:direction&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;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note that we are using user supplied parameters directly in an ActiveRecord query without sanitizing them. There is nothing stopping a malicious user from attempting an attack by passing in dangerous values to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;column&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; params. Fortunately, Rails automatically &lt;a href=&quot;https://github.com/rails/rails/blob/6f0d1ad14b92b9f5906e44740fce8b4f1c7075dc/activerecord/lib/active_record/relation/query_methods.rb#L597&quot;&gt;sanitizes parameters&lt;/a&gt; passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt; to keep us safe.&lt;/p&gt;

&lt;p&gt;Since users can sort by any column and in ascending or descending order, we should tell them which column they are currently sorting by, and in which direction. Let’s finish up this tutorial by adding a sort indicator to the table header. Head back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;thead&amp;gt;&lt;/code&gt; element:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;thead&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;show_sort_indicator_for&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;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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &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;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;show_sort_indicator_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;show_sort_indicator_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Seasons&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;/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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show_sort_indicator_for&lt;/code&gt; to each column header. This helper method will render a sort indicator next to the header text if the table is currently sorted by that column. That helper method does not exist yet, so head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt; to define it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_indicator&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;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;sort sort-&lt;/span&gt;&lt;span class=&quot;si&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;:direction&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;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;show_sort_indicator_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&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;:column&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;column&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show_sort_indicator_for&lt;/code&gt; renders the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_indicator&lt;/code&gt; helper when appropriate, otherwise it renders nothing.&lt;/p&gt;

&lt;p&gt;Finally, we need a little CSS for the sort indicator tag. Add the CSS in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/assets/stylesheets/application.tailwind.css&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span class=&quot;nc&quot;&gt;.sort&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;1rem&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;0.5rem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;width&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;nl&quot;&gt;height&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;nl&quot;&gt;border-left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;6px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&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;nl&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;6px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&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;span class=&quot;nc&quot;&gt;.sort-desc&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&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;nc&quot;&gt;.sort-asc&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-bottom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With this last change in place, refresh the players index page and see that you can sort columns in both directions. As you apply sorting, you will see that your scroll position is preserved and a nice little arrow appears to show the user which column they are sorting by, and in which direction.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-8-refresh-sorting-3.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;That is all for this tutorial, thank you for following along! As always, you can find the complete code for this tutorial &lt;a href=&quot;https://github.com/DavidColby/turbo_8_refresh_sorting/tree/finished-project&quot;&gt;on Github&lt;/a&gt;. Great work!&lt;/p&gt;

&lt;hr /&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Today we learned how we can use page morphing and scroll preservation in Turbo 8 to build a simple table filtering mechanism in Ruby on Rails. This implementation can replace the more complex versions of this experience that could be built in previous versions of Turbo. Implementations without page refreshing typically relied on Turbo Frames or Turbo Streams to maintain scroll position and provide the snappy-feel that we can now get out of the box with Turbo 8 and plain old Rails code.&lt;/p&gt;

&lt;p&gt;If you want to learn more about Turbo 8 and page refreshing, you may find these resources useful:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;I wrote &lt;a href=&quot;https://www.colby.so/posts/turbo-8-morphing-refreshes-on-rails&quot;&gt;this piece&lt;/a&gt; to dive into more detail about morphing page refreshes on Rails&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://dev.37signals.com/page-refreshes-with-morphing-demo/&quot;&gt;announcement post&lt;/a&gt; from Jorge Manrubia provides more details about the motivations and real world use cases for refreshing&lt;/li&gt;
  &lt;li&gt;Also linked above, &lt;a href=&quot;https://github.com/hotwired/turbo-site/pull/178&quot;&gt;this PR&lt;/a&gt; on the Turbo docs is useful for tracing through some of the code that powers refreshes&lt;/li&gt;
  &lt;li&gt;Finally, reading the &lt;a href=&quot;https://github.com/hotwired/turbo/pull/1019&quot;&gt;PR that added refreshes to Turbo&lt;/a&gt; is worthwhile if you want to understand how the underlying tools are implemented in JavaScript to make the magic happen&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Turbo 8 morphing refreshes on Rails</title>
   <link href="https://colby.so/posts/turbo-8-morphing-refreshes-on-rails"/>
   <updated>2024-03-17T00:00:00+00:00</updated>
   <id>https://colby.so/posts/turbo-8-morphing-refreshes-on-rails</id>
   <content type="html">&lt;p&gt;Turbo 8 was &lt;a href=&quot;https://dev.37signals.com/turbo-8-released/&quot;&gt;released in February of 2024&lt;/a&gt;, to much excitement in the Rails world, and the broader HTML-over-the-wire community. At the top of the list of new features was the &lt;a href=&quot;https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/&quot;&gt;combination of tooling&lt;/a&gt; that added support for morphing page refreshes and scroll position preservation, powered by &lt;a href=&quot;https://github.com/bigskysoftware/idiomorph&quot;&gt;idiomorph&lt;/a&gt; and the ability to broadcast page refreshes with a &lt;a href=&quot;https://turbo.hotwired.dev/reference/streams#refresh&quot;&gt;new Turbo Stream action&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This combination offers developers a “happier path” to partial page updates. Turbo Stream DOM manipulation actions (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prepend&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt;) work well, but they require maintaining separate code paths and have limitations when broadcasting data in applications that need to care about authorization and access levels inside of a team or company setting.&lt;/p&gt;

&lt;p&gt;Morphing refreshes are a powerful new tool in the Turbo (and turbo-rails) toolbox but they require a bit of a mental shift to understand and they are not without their drawbacks. Today we are going to explore Turbo 8’s new refresh feature in the context of a Rails application, learning how to use page refreshes, how to use broadcasted refreshes to push updates to all interested users at once, and dig into the pros and cons of refreshes compared to Turbo Stream actions that manipulate the DOM.&lt;/p&gt;

&lt;p&gt;Before we begin, this article assumes you have written Rails code before and that you have a basic level of understanding with Turbo and how to use it within a Rails application. If you are not a Rails developer, you may have a hard time following the code at some points.&lt;/p&gt;

&lt;p&gt;Let’s dive in.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h3 id=&quot;application-setup&quot;&gt;Application Setup&lt;/h3&gt;
&lt;p&gt;We are going to build a simple blog + post data model to try out page refreshing, starting from a new Rails 7 application with Tailwind CSS. Tailwind is not required for any part of this article, but we will add it to have nicer looking pages as we work.&lt;/p&gt;

&lt;p&gt;If you prefer to skip past the setup and go right to refreshing, you can &lt;a href=&quot;https://github.com/DavidColby/turbo-refreshes-on-rails&quot;&gt;clone this repository&lt;/a&gt; and skip ahead to the next section. We will move rapidly through the setup code, which should all be familiar if you are comfortaable working in Turbo-powered Rails applications.&lt;/p&gt;

&lt;p&gt;Get started from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new refreshable-blog &lt;span class=&quot;nt&quot;&gt;--css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;tailwind
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;refreshable-blog
bin/setup&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;After creating the new application, generate the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blog&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Post&lt;/code&gt; models:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bin/rails g scaffold Blog name:string
bin/rails g model Post name:string blog:references
bin/rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then generate a controller for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Posts&lt;/code&gt; and the views we will need to create and render posts from a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blog&lt;/code&gt; show page. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bin/rails g controller Posts
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/posts/_form.html.erb app/views/posts/create.turbo_stream.erb app/views/posts/_post.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Posts belong to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blog&lt;/code&gt;, so update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blog&lt;/code&gt; model to reflect that association:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Blog&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;:posts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Posts&lt;/code&gt; are only visible from their parent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Blog&lt;/code&gt;, so we can update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/routes.rb&lt;/code&gt; to define the proper nested routes for our application:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;:blogs&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;:posts&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Can be used by load balancers and uptime monitors to verify that the app is live.&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;up&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;rails/health#show&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :rails_health_check&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Defines the root path route (&quot;/&quot;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;blogs#index&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’ll build our views, starting with the posts form at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/posts/_form.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&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;s2&quot;&gt;&quot;post_form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;model: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;blog&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;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contents&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;k&quot;&gt;if&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;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error_explanation&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3&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&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluralize&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;errors&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error&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; prohibited this post from being saved:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&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;errors&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;error&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;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-5&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&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;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full&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;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&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Just regular old ERB and Tailwind here, with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; to identify it in the DOM, which we need for targeting a Turbo stream &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; call when the form is submitted. Next up, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/posts/_post.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;post&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Complicated stuff there.&lt;/p&gt;

&lt;p&gt;Next fill in our Turbo Stream erb template at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/posts/create.turbo-stream.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;post_form&apos;&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;s1&quot;&gt;&apos;posts/form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;post: &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;blog: &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;blog&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;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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;posts&apos;&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;s1&quot;&gt;&apos;posts/post&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;post: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This view will be rendered when a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post&lt;/code&gt; is created. In it, the two Turbo Stream actions &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post_form&lt;/code&gt; form to clear whatever data the user entered on the form and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;append&lt;/code&gt; the new post to the list of posts.&lt;/p&gt;

&lt;p&gt;Next, we want to render posts and the new post form on the blog show page. Let’s head there next and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mx-auto md:w-2/3 w-full flex&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;mx-auto&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;@blog&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;s2&quot;&gt;&quot;Back to blogs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;blogs_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;ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium&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;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;mx-auto md:w-2/3 w-full 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;h2&amp;gt;&lt;/span&gt;Blog posts&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;posts&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;@blog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;posts/post&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;posts/form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &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;presence&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;blog: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@blog&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;More standard ERB here, with some Tailwind classes provided by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/rails scaffold&lt;/code&gt;. Before this will work, we need to implement the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; action in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostsController&lt;/code&gt;. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controller/posts_controller.rb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;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;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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_params&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;blog_id&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;:blog_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&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;k&quot;&gt;if&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;save&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;turbo_stream&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;else&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;turbo_stream&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;s1&quot;&gt;&apos;post_form&apos;&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;s1&quot;&gt;&apos;posts/form&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;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;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;post_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;:post&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; action runs, successful form submissions render a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; view (that’s the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt; file we created earlier). When errors are present, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream.replace&lt;/code&gt; action runs to replace the form content with any relevant errors.&lt;/p&gt;

&lt;p&gt;This implementation of creating and inserting new objects using Turbo and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; is a common way to leverage Turbo in a Rails application today and will be our starting point for exploring how Turbo refreshes can be leveraged to simplify our code without sacrificing user experience.&lt;/p&gt;

&lt;p&gt;Before ending the setup section, head over to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt; to adjust the base page layout:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&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;title&amp;gt;&lt;/span&gt;RefreshableBlog&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&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;viewport&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width=device-width,initial-scale=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;csrf_meta_tags&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;csp_meta_tag&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;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tailwind&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;inter-font&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&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;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&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;javascript_importmap_tags&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;main&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;container mx-auto mt-28 px-5&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;yield&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we dropped a flex class from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; element so that the form shows up under the list of posts instead of beside the list of posts. Why that flex is there in the default Tailwind + Rails layout is anyone’s guess, but it is gone from our application now.&lt;/p&gt;

&lt;p&gt;If all has gone well with setup, you should be able to start up your Rails server with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; and visit http://localhost:3000. Create at least one blog, head to that blog’s show page, and add a few posts to see that inserting new posts with Turbo Streams works as expected and then move on to the next section where we will start adding Turbo refreshes to our application.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-refreshes-on-rails-1.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;h2 id=&quot;refreshing-posts&quot;&gt;Refreshing posts&lt;/h2&gt;
&lt;p&gt;Our goal in this section is to replace the current method of inserting new posts on the blog show page with a Turbo-powered morphing refresh. This will require much less code than you might expect.&lt;/p&gt;

&lt;p&gt;Start by updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostsController&lt;/code&gt; at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/posts_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;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;@blog&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Blog&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;:blog_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;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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_params&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;blog&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@blog&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&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;save&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@blog&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;s1&quot;&gt;&apos;blogs/show&apos;&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;post_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;:post&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we redirect to the blog show page if the new post is created. If the post cannot be saved we render the blog show page again with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;422&lt;/code&gt; status code. This is standard Rails, there is no new magic required in the controller to use page refreshing, we are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirecting&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rendering&lt;/code&gt; as we have been for more than a decade.&lt;/p&gt;

&lt;p&gt;If you go to a blog show page now and submit the post form, you will notice that everything works except that the redirect is a full, normal page turn, meaning you lose your scroll position. Any ephemeral on-page changes (like opening a dropdown menu or interacting with a form element) would also be lost. We have done a full page turn and redirected the browser to a new page, so that’s all expected behavior but the user experience of jumping to the top of the page when they submitted a form to add a new post is not great.&lt;/p&gt;

&lt;p&gt;Let’s fix that by telling Turbo to using morphing for refreshes and to maintain scroll position when a page refresh occurs. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&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;title&amp;gt;&lt;/span&gt;RefreshableBlog&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&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;viewport&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width=device-width,initial-scale=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;csrf_meta_tags&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;csp_meta_tag&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;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tailwind&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;inter-font&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&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;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&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;javascript_importmap_tags&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_refreshes_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;method: :morph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;scroll: :preserve&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;yield&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:head&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;main&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;container mx-auto mt-28 px-5&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;yield&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added two lines to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_refreshes_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;method: :morph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;scroll: :preserve&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;yield&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:head&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/102a491754d46f7dd924201fcfaf879a0f04b11c/app/helpers/turbo/drive_helper.rb#L70C7-L70C27&quot;&gt;helper method&lt;/a&gt; adds the &lt;a href=&quot;https://turbo.hotwired.dev/reference/attributes#meta-tags&quot;&gt;meta tags&lt;/a&gt; Turbo looks for to enable morphing and scroll preservation on a page. In the source, you will note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_refreshes_with&lt;/code&gt; uses &lt;a href=&quot;https://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html#method-i-provide&quot;&gt;provide&lt;/a&gt; to add content to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head&lt;/code&gt;, which will not be rendered until &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield :head&lt;/code&gt; is called. If you have encountered issues using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_refreshes_with&lt;/code&gt; without the following &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; call and gotten frustrated at the difference in this helper and the other meta tag helpers in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; which do not require a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; call after, &lt;a href=&quot;https://github.com/hotwired/turbo-rails/pull/542&quot;&gt;the discussion&lt;/a&gt; on this (later reverted) PR may be interesting. You can also use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_refresh_method_tag&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_refresh_scroll_tag&lt;/code&gt; to set the refresh method and scroll handling meta tags individually, or just add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;meta&amp;gt;&lt;/code&gt; tags directly without using helper methods, if that’s your style.&lt;/p&gt;

&lt;p&gt;You should also note that we added the meta tags into the application layout, which means they will be there on every page in our application. In a real application, you may find that setting these values on every page can have unintended consequences. If you are adopting morphing refreshes and the accompanying scroll preservation mechanic in an existing application, it is wise to add these meta tags into the head only on pages that require morphing and scroll preservation to avoid unexpected behavior on pages that should not attempt to morph content or preserve scroll position.&lt;/p&gt;

&lt;p&gt;Refresh the blog show page, add a new post and see that submitting the form and following the redirect does not lose scroll position. That’s the all of the work that it takes to use morphing page refreshes with Turbo and Rails. There are no more steps, there is no more code to write. Magical.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-refreshes-on-rails-2.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Let’s pause here and talk through what’s happening. In particular, what triggers a morphing refresh before we move on to fancier use cases, including broadcasting refreshes to other users of the application.&lt;/p&gt;

&lt;p&gt;Users of our application visit a blog show page, at a url like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/blogs/12&lt;/code&gt;. On the page, we render a form to create a new post. When that form is submitted, two outcomes are possible:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;If the post is saved, we &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect&lt;/code&gt; to the blog show page again, so the original and the destination URL match.&lt;/li&gt;
  &lt;li&gt;If the post is not saved, we render the blog show page again without redirecting anywhere, so the original and the destination URL match.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In both cases, the original URL and the destination URL match. This matching is the key that unlocks Turbo refreshes. A morphing refresh triggered by a normal response/request cycle &lt;a href=&quot;https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/core/drive/page_view.js#L19&quot;&gt;will only occur&lt;/a&gt; when the user starts and ends on the same page (with the &lt;a href=&quot;https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/core/drive/page_view.js#L58&quot;&gt;same URL&lt;/a&gt;). If we are navigating between different pages, at different URLs, Turbo Drive won’t attempt to morph anything. This makes sense, I think, but it is worth stating it. Turbo refreshes are refreshing the same content, with changes that are morphed in using idiomorph.&lt;/p&gt;

&lt;p&gt;You can use morphing refreshes for pages like our blog show page that allow users to manage child content or otherwise make changes that a user would expect to keep them in the current context of the page. If you have a more traditional CRUD application with full new and edit pages, those pages will not benefit from refreshing.&lt;/p&gt;

&lt;p&gt;For a thoughtful, more in-depth discussion of what page refreshes are and how they work, I found &lt;a href=&quot;https://jonsully.net/blog/turbo-8-page-refreshes-morphing-explained-at-length/&quot;&gt;this piece&lt;/a&gt; by Jon Sully to be a valuable read.&lt;/p&gt;

&lt;p&gt;So far, we have seen how we can use Turbo refreshes and morphing to replace Turbo Streams rendered from a controller without sacrificing user experience. Our implementation of refreshes so far is still within the normal request/response cycle for a single user. The user submits a form, and we send back an HTML payload that Turbo processes with idiomorph. This is valuable, but we can unlock more power by broadcasting refresh actions to all users with Turbo Streams.&lt;/p&gt;

&lt;h2 id=&quot;lets-see-how-it-works&quot;&gt;Let’s see how it works.&lt;/h2&gt;
&lt;h2 id=&quot;broadcast-refreshes&quot;&gt;Broadcast refreshes&lt;/h2&gt;
&lt;p&gt;Broadcasting Turbo Stream actions via a WebSocket connection allows us to push real-time updates out to all interested users as data in our application changes. In our application, we want to broadcast changes to posts each time a post is created, updated, or deleted. Any user on the post’s blog show page should see those updates as they happen.&lt;/p&gt;

&lt;p&gt;Start by updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Post&lt;/code&gt; model at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/post.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Post&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;:blog&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;broadcasts_refreshes_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:blog&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb#L209&quot;&gt;broadcasts_refreshes_to&lt;/a&gt; adds an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_commit&lt;/code&gt; callback that broadcasts a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh&lt;/code&gt; &lt;a href=&quot;https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/core/streams/stream_actions.js#L38&quot;&gt;Turbo Stream action&lt;/a&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh&lt;/code&gt; action in turn &lt;a href=&quot;https://github.com/hotwired/turbo/blob/600203edf6a7fdba328bfbc9ca8c62354c7d3a27/src/core/session.js#L110&quot;&gt;performs&lt;/a&gt; a &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive#application-visits&quot;&gt;replace&lt;/a&gt; visit to fetch updated content for the same URL from the server and update the page with the new content on each message recipient’s browser. Thanks to the magic of morphing, this all happens without the recipient losing their scroll position or the current state of the page — updated content just appears seamlessly.&lt;/p&gt;

&lt;p&gt;To make this work, we have one more piece to add. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/blogs/show.html.erb&lt;/code&gt; and update the posts section of the page like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream_from&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@blog&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;vi&quot;&gt;@blog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;posts/post&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream_from&lt;/code&gt; call, subscribing to a channel specific to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@blog&lt;/code&gt; we are viewing. This channel matches the channel we are broadcasting on in the model — the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:blog&lt;/code&gt; symbol translates into a call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post.blog&lt;/code&gt;. Since we are viewing the show page for the blog that the post belongs to, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post.blog&lt;/code&gt; equals &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@blog&lt;/code&gt;, and so our users will be subscribed to the right channel to see updates for the blog they are viewing.&lt;/p&gt;

&lt;p&gt;It is important to understand that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcasted&lt;/code&gt; refreshes do not care about what page the user is on. To use morphing refreshes in the normal request/response cycle, like we did in the last section, the original and destination URL need to match. When we are broadcasting refreshes to users, the Turbo Stream &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh&lt;/code&gt; action prompts Turbo to go fetch the user’s current URL again, as long as they are on a page that subscribes to the relevant stream channel, they will get updated content for that page. Again, this makes sense I believe, but it is important to understand that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh&lt;/code&gt; Stream Actions are different than morphing page refreshes.&lt;/p&gt;

&lt;p&gt;Let’s pause here and see that broadcasting works. Open up the same blog show page in two browsers. In one browser, create a new post. If all has gone well, the post will appear in the list of posts on the second browser without changing scroll position.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-refreshes-on-rails-3.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Neat. We have now seen how to use Turbo 8’s morphing refreshes in the context of a regular request/response cycle and using Turbo Stream broadcasts in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt;. Broadcasted refreshes are a powerful tool, but they can have some surprising behavior. Let’s demonstrate one of the most common gotchas now, and discuss how to work around this behavior.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/posts_controller.rb&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; action to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sleep&lt;/code&gt;, to make it easier to see the problem:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@blog&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Blog&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;:blog_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;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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_params&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;blog&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@blog&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&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;save&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@blog&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;s1&quot;&gt;&apos;blogs/show&apos;&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The sleep here adds 5 seconds of wait time before a form submission processes, which gives us enough time to submit the form in one window and then switch over and start working on the form in the other window. Try it out now — submit the form in one window and then move over to the other window and start typing in the post name field in the other window.&lt;/p&gt;

&lt;div style=&quot;display: flex; justify-content: center; margin-bottom: 1em;&quot;&gt;
  &lt;video controls=&quot;&quot; width=&quot;640&quot; height=&quot;480&quot;&gt;
    &lt;source src=&quot;/public/videos/turbo-refreshes-on-rails-4.mp4&quot; type=&quot;video/mp4&quot; /&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;This is not a great experience for your users! Saving scroll position is great, but if you wipe out a user’s work every time a broadcasted refresh runs, they are not going to be happy.&lt;/p&gt;

&lt;p&gt;Why does this happen? Idiomorph is smart and only updates the parts of the page that are different when a refresh runs; however, form inputs are tricky. When the new page content comes back, the form will always be a clean,  default version of the form because that is what the server knows about; the server does not know that the user is currently halfway through writing a novel in a form input. Idiomorph sees the clean version of the form, sees that the current version of the page is different, and replaces the old, active version of the form with the new, empty version. Woof.&lt;/p&gt;

&lt;p&gt;There are ways to fix this behavior, most commonly reaching for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-permanent&lt;/code&gt; attribute. This attribute &lt;a href=&quot;https://turbo.hotwired.dev/handbook/page_refreshes.html#exclude-sections-from-morphing&quot;&gt;skips morphing&lt;/a&gt; of the element and any child elements. In our particular use case, since we are relying on Turbo refreshing to update the browser of the user that submitted the form and broadcasting those changes as refreshes to every other user, we need more than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-permanent&lt;/code&gt; to make it work. Adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-permanent&lt;/code&gt; to a wrapped div around the form inputs will fix the issue of lost focus that we saw in the above demonstration. However, that change alone will also cause the form not to reset for the user who submitted the form in the first place, requiring them to delete whatever they typed into the form or making it look like something broke, which is not an acceptable user experience.&lt;/p&gt;

&lt;p&gt;A complete fix for this behavior in our use case could be both a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-permanent&lt;/code&gt; element and the introduction of a Stimulus controller to clear the form for the user that made the original submission.&lt;/p&gt;

&lt;p&gt;A separate article will dive further into the details of handling forms in refreshes, but it is important to think about morphing and how it will affect users as you begin to build more complex user interfaces. Modals, drawers, drop down menus, forms, and third party JavaScript libraries that dynamically add elements to the DOM can have surprising behavior when combined with morphing refreshes.&lt;/p&gt;

&lt;p&gt;That’s all of the code we are going to write today, nice work following along!&lt;/p&gt;

&lt;p&gt;We will wrap up today by talking through some of the pros and cons of using morphing refreshes, and broadcasted refreshes in your Turbo-powered Rails applications. You can see the full code for today’s article &lt;a href=&quot;https://github.com/DavidColby/turbo-refreshes-on-rails/tree/finished-project&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;pros-and-cons-of-turbo-refreshes&quot;&gt;Pros and cons of Turbo Refreshes&lt;/h2&gt;
&lt;p&gt;From my perspective, the two biggest wins from Turbo morphing refreshes, and the accompanying broadcasted refresh in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Simpler happy paths in the code. With refreshes we do not need to worry about setting DOM ids (and hoping they aren’t removed or duplicated in the future), maintaining separate paths in our controllers, or creating and maintaining &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream.erb&lt;/code&gt; view files. Write regular old Rails code and shove a couple of meta tags into the head on pages that should use morphing and you are good to go.&lt;/li&gt;
  &lt;li&gt;Broadcasted refreshes solve the session context problem that is one of the &lt;a href=&quot;https://www.reddit.com/r/rails/comments/11w7hut/broadcasting_to_multiple_streams_and_rendering_a/&quot;&gt;most common stumbling blocks&lt;/a&gt; for developers learning to use Turbo Stream broadcasts. Because a refresh broadcast is a ping to tell the recipient browser to go fetch new content for the page, we can broadcast updates without worrying about exposing data or functions that should be hidden. While there were &lt;a href=&quot;https://www.colby.so/posts/conditional-rendering-with-turbo-stream-broadcasts&quot;&gt;ways&lt;/a&gt; to &lt;a href=&quot;https://github.com/palkan/action_policy/discussions/254&quot;&gt;work around&lt;/a&gt; the lack of session context in stream broadcasts that deliver rendered content in the original message, those workarounds are more complex and provide a worse user and developer experience than than the simple, clean implementation we get with broadcasted refreshes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we saw with the challenge of keeping form state intact, there are tradeoffs and challenges that come with morphing refreshes:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Form inputs and dynamic page content can be tricky to deal with, requiring more code and a carefully planned architecture to get the most benefit from refreshes. Needing to write extra code to support refreshes gets off of the happy path sold as a key benefit of morphing refreshes.&lt;/li&gt;
  &lt;li&gt;Refreshes are more resource heavy than other Turbo Stream actions. In a standard Turbo Stream action, the server renders only the content that needs to be updated and nothing more, and much less work is required by the user’s browser to update the DOM. Rendering the entire page and then asking the browser to diff the entire page and update it in real-time is not free for you or for your users, especially if they are on &lt;a href=&quot;https://danluu.com/slow-device/&quot;&gt;lower end devices&lt;/a&gt; without the processing power that our development machines have. While we may not &lt;em&gt;need&lt;/em&gt; to care about speed and costs on small personal projects, if we are building real applications, we should consider what the cost is for us (rendering lots of full pages when 95% of the content is identical) and to our users (who suddenly have more work for their browsers to do) for the cleaner code paths that morphing refreshes provide.&lt;/li&gt;
  &lt;li&gt;There are also speed differences here with broadcasted refreshes. A broadcasted refresh requires the original WebSocket message to be sent to tell the recipients browser to fetch new content and then a round trip to the server to fetch that content and replace the current page. With a traditional Turbo Stream broadcast, the updated DOM content is contained in the original WebSocket message. Cutting out that round trip means that a Turbo Stream &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;append&lt;/code&gt; broadcast will get updated content to the user faster than a Turbo Stream &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh&lt;/code&gt; broadcast. In many applications this speed different will not matter. The user is getting updates in the background and does not know an update is coming, but for some use cases refreshes will feel slower than other broadcast actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turbo 8’s morphing refreshes are an exciting new tool in our toolbox, and as they mature and the community coalesces around best practices and expands the Turbo feature set around morphing, we can expect to see real value in production applications from adopting refreshes.&lt;/p&gt;

&lt;p&gt;As always with shiny new developer tools, I encourage you to think about your use case and what value refreshes add to your application. For many existing applications that rely on Turbo Streams, the incremental value added by replacing all of that code with refreshes is not likely to be worth the effort; however, you may find that in new applications, or as you add new features to your existing application, relying on morphing refreshes lets you move faster without sacrificing the single page application feel that Turbo Streams provide.&lt;/p&gt;

&lt;p&gt;For further reading on Turbo 8’s changes, the motivation behind the changes, and a deeper dive into how refreshes and morphing work, you may find these resources valuable:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/&quot;&gt;A happier path in Turbo with morphing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Jon Sully’s &lt;a href=&quot;https://jonsully.net/blog/turbo-8-page-refreshes-morphing-explained-at-length/&quot;&gt;deep dive into Turbo page refreshes and morphing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The Basecamp &lt;a href=&quot;https://github.com/basecamp/turbo-8-morphing-demo/branches/all&quot;&gt;turbo-8-morphing-demo repo&lt;/a&gt; (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; branch is empty here, check the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page-refreshes&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stream-actions&lt;/code&gt; branches for comparisons between approaches)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for this article, thank you for reading! Turbo morphing and refreshing are very new and some functionality is still in flux. If I have missed something or you see a better approach, &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;reach out&lt;/a&gt; and share your thoughts.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a modal form with Turbo Stream GET requests and custom stream actions</title>
   <link href="https://colby.so/posts/building-modal-forms-with-turbo-streams"/>
   <updated>2022-09-24T00:00:00+00:00</updated>
   <id>https://colby.so/posts/building-modal-forms-with-turbo-streams</id>
   <content type="html">&lt;p&gt;Turbo 7.2 brought major changes to what you can accomplish with Turbo Streams. To demonstrate those changes, today we are going to build a Turbo Stream-powered modal form without a single Turbo Frame. We will also create our own custom Turbo Stream action that emits a custom event on the browser.&lt;/p&gt;

&lt;p&gt;The key technique we will use in this article is an update to the standard method of using Turbo to open and populate modals. Prior to Turbo 7.2, Turbo would not allow GET requests to respond directly with Turbo Streams. Smart folks devised a workaround to this limitation which involved using empty Turbo Frames to sneak in Turbo Stream actions. That workaround still works fine, but it is no longer necessary.&lt;/p&gt;

&lt;p&gt;As part of our modal implementation, we will also have a chance to build a custom &lt;a href=&quot;https://github.com/hotwired/turbo/pull/479&quot;&gt;Turbo Stream action&lt;/a&gt;. Custom actions are an entirely new feature added in Turbo 7.2, and we will cover enough to get you started with this new tool in Turbo.&lt;/p&gt;

&lt;p&gt;When we are finished our application work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-24-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page clicking a button to open a modal dialog window on the page. They fill in the sole text input on the form and submit the form. The modal dialog closes and the page beneath is updated with the record the user just created.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To follow along with this article, you will need to be comfortable with Ruby on Rails and you should have some experience using &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt;. We will move quickly through the Turbo content, so previous exposure to using Turbo Streams will be helpful but is not required. As usual, you can review the complete code for this tutorial &lt;a href=&quot;https://github.com/DavidColby/turbo-stream-modals/tree/complete&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s get started!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h3 id=&quot;application-setup&quot;&gt;Application setup&lt;/h3&gt;
&lt;p&gt;We will start with a new Rails 7 application with esbuild and Tailwind along with Turbo 7.2 and Stimulus. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new turbo-stream-modals &lt;span class=&quot;nt&quot;&gt;--javascript&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;esbuild &lt;span class=&quot;nt&quot;&gt;--css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;tailwind
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;turbo-stream-modals&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note that neither Tailwind or esbuild are hard requirements, just personal preferences. The Turbo Stream and Stimulus techniques will work fine here with import maps, Vite, and any CSS framework you like.&lt;/p&gt;

&lt;p&gt;For this exercise, users will be creating “cards”, which we will scaffold up using the Rails generator. From your terminal again:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g scaffold Card name:string
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Start up the server with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; from your terminal.&lt;/p&gt;

&lt;p&gt;Open the code up in your favorite editor and head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/cards/index.html.erb&lt;/code&gt; for some non-essential layout updates:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;max-w-3xl mx-auto mt-8&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 justify-between items-baseline mb-6&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;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Cards&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Card&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_card_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;text-blue-600&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;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;card-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 items-baseline space-y-6 p-4 shadow-lg rounded&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;@cards&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;card&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;card&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;card: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;card&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;More small layout updates in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/cards/_card.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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-gray-700 border-b border-gray-200 w-full pb-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;n&quot;&gt;card&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With cards scaffolded up and the views cleaned up a bit, we are ready to start building the modal.&lt;/p&gt;

&lt;h3 id=&quot;adding-new-cards-in-a-modal&quot;&gt;Adding new cards in a modal&lt;/h3&gt;
&lt;p&gt;To open and close the card form modal, we are going to use a Stimulus controller.&lt;/p&gt;

&lt;p&gt;Generate that controller from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g stimulus modal&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill that new Stimulus controller in at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/modal_controller.js&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// This controller is an edited-to-the-essentials version of the modal component created by @excid3 as part of the essential tailwind-stimulus-components package found here:&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// https://github.com/excid3/tailwindcss-stimulus-components&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;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;nc&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;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;container&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;nf&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;toggleClass&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;hidden&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;modal-background&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundHtml&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;nf&quot;&gt;_backgroundHTML&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;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;nf&quot;&gt;close&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;open&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;body&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;nf&quot;&gt;add&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;fixed&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;inset-x-0&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;overflow-hidden&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;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;nf&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;toggleClass&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;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;insertAdjacentHTML&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;beforeend&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundHtml&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;background&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;nf&quot;&gt;querySelector&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;backgroundId&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;nf&quot;&gt;close&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;typeof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;undefined&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;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;preventDefault&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;nf&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;toggleClass&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;background&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;background&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;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;_backgroundHTML&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;`&amp;lt;div id=&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;backgroundId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; class=&quot;fixed top-0 left-0 w-full h-full&quot; style=&quot;background-color: rgba(0, 0, 0, 0.7); z-index: 9998;&quot;&amp;gt;&amp;lt;/div&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;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;As the comment notes, this code is a stripped-down version of the production-ready modal provided in Chris Oliver’s &lt;a href=&quot;https://github.com/excid3/tailwindcss-stimulus-components&quot;&gt;tailwindcss-stimulus-components library&lt;/a&gt;. I modified it to remove the extras that we don’t need for our project and to save us a few chunks of copy/pasted code that we would need if we were using the full library.&lt;/p&gt;

&lt;p&gt;The JavaScript in this controller is fairly standard stuff, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;open&lt;/code&gt; opens the modal and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;close&lt;/code&gt;, shockingly, closes the modal by applying the right Tailwind classes and toggling in and out a background overlay.&lt;/p&gt;

&lt;p&gt;To use this new Stimulus controller, we need to connect it to the DOM. Head over to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&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;title&amp;gt;&lt;/span&gt;TurboStreamModals&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&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;viewport&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width=device-width,initial-scale=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;csrf_meta_tags&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;csp_meta_tag&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;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&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;javascript_include_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;defer: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;body&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modal&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;yield&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;s2&quot;&gt;&quot;shared/modal&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;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we connected the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal&lt;/code&gt; controller to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt;. Inside of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt;, we render a modal partial that does not exist yet.&lt;/p&gt;

&lt;p&gt;Create that partial next, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;app/views/shared
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/shared/_modal.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill in the modal partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-modal-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;hidden fixed inset-0 overflow-y-auto flex items-center justify-center&quot;&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;z-index: 9999;&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;max-w-lg max-h-screen w-full relative&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;m-1 bg-white rounded shadow&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;px-4 py-5 border-b border-gray-200 sm:px-6&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;h3&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modal-title&quot;&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-6 font-medium text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/h3&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modal-body&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The key pieces in this partial are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-target&lt;/code&gt; data attribute on the container div. The Stimulus controller uses this attribute to know which element to apply classes to when the modal is opened and closed.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-title&lt;/code&gt; header and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-body&lt;/code&gt; div, both of which are empty when the modal partial is rendered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-title&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-body&lt;/code&gt; elements will be filled in with content from the server when we open the modal using the magic of Turbo Streams.&lt;/p&gt;

&lt;p&gt;Next update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/cards/index.html.erb&lt;/code&gt; to adjust the New Card link to open a modal when clicked:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;max-w-3xl mx-auto mt-8&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 justify-between items-baseline mb-6&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;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Cards&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Card&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_card_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;text-blue-600&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;click-&amp;gt;modal#open&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_stream: &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;/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 items-baseline space-y-6 p-4 shadow-lg rounded&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;@cards&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;card&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;card&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;card: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;card&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the addition here of two data attributes on the “New Card” &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; attribute tells Stimulus to fire the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal#open&lt;/code&gt; method in the modal controller, making the modal visible to the user. The other attribute, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; tells Turbo to send the request to the server as a Turbo Stream request, instead of a standard HTML request.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-streams&lt;/code&gt; is &lt;a href=&quot;https://github.com/hotwired/turbo/pull/612&quot;&gt;new in Turbo 7.2&lt;/a&gt;. This addition opens up the ability to respond to GET requests with Turbo Stream actions directly, instead of trojan horsing actions inside of empty Turbo Frames. This change allows us to write a little less code which is nice. Even better, it makes our intentions as developers more clear. The empty Turbo Frame work around was a confusing piece of indirection that we no longer need to rely on.&lt;/p&gt;

&lt;p&gt;With the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; attribute added, Turbo will expect a Turbo Stream response from the server when the new card link is processed. To make this work, we need to tell Rails what we want to render in response to a Turbo Stream request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cards/new&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/cards/new.turbo_stream.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill in the new view:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;update&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;modal-title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Add a card&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;update&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;modal-body&quot;&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;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;card: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@card&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here our “view” is really two Turbo Stream actions. These actions will be picked up by Turbo’s JavaScript on the frontend and processed to update the DOM.&lt;/p&gt;

&lt;p&gt;In this case, that means we will send over content for the empty header with an id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-title&lt;/code&gt; and the empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-body&lt;/code&gt; div. Turbo makes those updates, and our modal magically gets the updated, server-rendered content.&lt;/p&gt;

&lt;p&gt;Update the cards form partial at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/cards/_form.html.erb&lt;/code&gt; before checking to see if our modal works:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@card&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;s2&quot;&gt;&quot;card-form&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;px-4 pt-5 pb-4 sm:p-6 sm:pb-4&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;@card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;p-4 border border-red-600&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;ul&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_messages&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;message&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;n&quot;&gt;message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&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;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&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&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-b mt-6 px-4 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;w-full sm:col-start-2 bg-blue-600 px-4 py-2 mb-4 text-white rounded-sm hover:bg-blue-700&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;button&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;modal#close&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mt-3 w-full sm:mt-0 sm:col-start-1 mb-4 bg-gray-100 hover:bg-gray-200 rounded-sm px-4 py-2&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;/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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Refresh the cards index page at &lt;a href=&quot;http://localhost:3000/cards&quot;&gt;http://localhost:3000/cards&lt;/a&gt;, click on the “New Card” link and, if all has gone well so far, a modal should open with the new card header text and the card form populated.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-24-open-modal.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page clicking a button to open a modal dialog window on the page.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;At this point submitting the card form will create a new card in the database but, after saving the card you will be redirected to the card show page. Redirecting defeats the purpose of opening the card form in the modal. Let’s fix this issue next.&lt;/p&gt;

&lt;p&gt;First, create another Turbo Stream view. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/cards/create.turbo_stream.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that view in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;prepend&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;card-list&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@card&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Easy enough. We use a Turbo Stream prepend action to insert the newly created card in the list.&lt;/p&gt;

&lt;p&gt;Head over to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/cards_controller.rb&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; method:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@card&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Card&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;card_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@card&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;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;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;card_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@card&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;Card was successfully created.&quot;&lt;/span&gt; &lt;span class=&quot;p&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;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;:show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: :created&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;vi&quot;&gt;@card&lt;/span&gt; &lt;span class=&quot;p&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;turbo_stream&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&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;p&quot;&gt;{&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;p&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;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;@card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The addition of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format.turbo_stream&lt;/code&gt; when a card saves ensures that inbound Turbo Stream requests render the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt; view that we added.&lt;/p&gt;

&lt;p&gt;By default, Turbo assumes all form submissions are Turbo Stream requests, so we don’t need to make any changes to our form for this to work, just having a Turbo Stream view is enough.&lt;/p&gt;

&lt;p&gt;Head back to the &lt;a href=&quot;http://localhost:3000/cards&quot;&gt;cards index&lt;/a&gt;, open the modal and save a card. You’ll see the newly created card added to the top of the list of cards but the modal stays open.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-24-modal-stuck.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page clicking a button to open a modal dialog window on the page. They fill in the sole text input on the form and submit the form. The modal dialog stays open. Behind the modal, the user can see their newly created record added to the page, if only the modal would close life would be grand.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Users will expect the modal to close when they submit the form, so let’s address that next.&lt;/p&gt;

&lt;h3 id=&quot;creating-a-custom-turbo-stream-action&quot;&gt;Creating a custom Turbo Stream action&lt;/h3&gt;
&lt;p&gt;We are going to use a custom Turbo Stream action to dispatch an event when the card form is submitted successfully. This event will be generated on the server, sent over the wire to the frontend, processed by Turbo, and picked up by a waiting Stimulus controller to close the modal.&lt;/p&gt;

&lt;p&gt;It’ll be pretty fancy. It will also be completely unnecessary. Turbo &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;emits an event&lt;/a&gt; after a form submission that would work just fine to close our modal. Building a custom Turbo Stream action to submit an event is purely for demonstration purposes.&lt;/p&gt;

&lt;p&gt;In the real world, if you need to close a modal after a Turbo Stream submission, the built-in event is just fine. It just isn’t as fun as rolling our own &lt;a href=&quot;https://github.com/hotwired/turbo/pull/479&quot;&gt;custom Turbo Stream action&lt;/a&gt; with the new tools provided for us in Turbo 7.2.&lt;/p&gt;

&lt;p&gt;To implement the custom action we need a new helper on the Rails side and a little bit of JavaScript to tell Turbo what to do when it receives our custom action in the browser. Let’s start with the JavaScript.&lt;/p&gt;

&lt;p&gt;From your terminal, create a new directory and file where we will define our custom event:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;app/javascript/custom_actions
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/javascript/custom_actions/dispatch_event.js&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event.js&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;StreamActions&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/turbo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;StreamActions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dispatch_event&lt;/span&gt; &lt;span class=&quot;o&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;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;name&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;nf&quot;&gt;getAttribute&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;name&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&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;nc&quot;&gt;Event&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;nb&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dispatchEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// If you want to send the event somewhere besides the window&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// document.getElementById(this.target).dispatchEvent(event) &lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we are importing StreamActions from Turbo and extending it to add a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt;  action to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StreamActions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt; takes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; attribute that we then use to define and dispatch an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events&quot;&gt;Event&lt;/a&gt; on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While dispatching the event on the window works fine for this tutorial, in the real world, you can scope the event to a particular element instead of blasting it out on the window. I’ve noted a simple approach to that problem as a comment.&lt;/p&gt;

&lt;p&gt;You might also want to define a new &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events#adding_custom_data_%E2%80%93_customevent&quot;&gt;CustomEvent&lt;/a&gt; instead of an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Event&lt;/code&gt;. Using a  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomEvent&lt;/code&gt; allows you to include additional data in the event payload. Again, we don’t need that extra data in this tutorial, but the possibility is there as you explore.&lt;/p&gt;

&lt;p&gt;Head over to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/application.js&lt;/code&gt; to import the new custom action:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/turbo-rails&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./controllers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./custom_actions/dispatch_event&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;That’s the JavaScript-side completed.&lt;/p&gt;

&lt;p&gt;Next we need to define a matching Turbo Stream helper tag on the server. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/helpers/dispatch_event_helper.rb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill in the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event_helper&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;DispatchEventHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;dispatch_event&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;turbo_stream_action_tag&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:dispatch_event&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;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;Turbo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Streams&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TagBuilder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prepend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;DispatchEventHelper&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Nothing fancy. We add a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt; helper and add it to the list of available Turbo Stream action tags.&lt;/p&gt;

&lt;p&gt;Both the JavaScript and Rails side of the equation here are derived from Marco Roth’s &lt;a href=&quot;https://github.com/hotwired/turbo-rails/pull/373&quot;&gt;example in the PR&lt;/a&gt; that made these custom helper tags possible in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt;. This work was instrumental in getting full-featured support for custom Turbo Stream actions into our hands.&lt;/p&gt;

&lt;p&gt;To use this new helper tag, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/create.turbo_stream.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;prepend&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;card-list&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@card&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;dispatch_event&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;modalClose&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With this change, the Turbo Stream payload will now include both a prepend action and our custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt; action, which will emit an event on the client side named “modalClose”.&lt;/p&gt;

&lt;p&gt;The last step to make this work is to fire the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;close&lt;/code&gt; method in the modal Stimulus controller when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modalClose&lt;/code&gt; event is received.&lt;/p&gt;

&lt;p&gt;Back in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/application.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&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;title&amp;gt;&lt;/span&gt;TurboStreamModals&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&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;viewport&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width=device-width,initial-scale=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;csrf_meta_tags&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;csp_meta_tag&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;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&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;javascript_include_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;data-turbo-track&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reload&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;defer: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;body&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modal&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modalClose@window-&amp;gt;modal#close&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;yield&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;s2&quot;&gt;&quot;shared/modal&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;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; attribute on the body listening for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modalClose&lt;/code&gt; custom event.&lt;/p&gt;

&lt;p&gt;Head back to the &lt;a href=&quot;http://localhost:3000&quot;&gt;cards index page&lt;/a&gt;, open the modal, create a new card, and see that the new card is prepended to the list and the modal closes. Incredible stuff.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-24-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page clicking a button to open a modal dialog window on the page. They fill in the sole text input on the form and submit the form. The modal dialog closes and the page beneath is updated with the record the user just created.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Let’s look closer at what is happening here by looking at the payload that the server sends over the wire when it renders &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That payload will look something like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;prepend&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;card-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;template&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-gray-700 border-b border-gray-200 w-full pb-2&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      Grim Patron
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modalClose&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;dispatch_event&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;template&amp;gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The payload contains &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-stream&lt;/code&gt; custom elements with an action set. Each element wraps a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;template&lt;/code&gt; tag, although the template for our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt; action is empty since that action does not need to render any HTML.&lt;/p&gt;

&lt;p&gt;Turbo’s JavaScript &lt;a href=&quot;https://github.com/hotwired/turbo/blob/main/src/core/streams/stream_actions.ts&quot;&gt;processes the payload&lt;/a&gt;. The custom action we defined in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/custom_actions/dispatch_event.js&lt;/code&gt; is used to process the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt; action in the payload.&lt;/p&gt;

&lt;p&gt;This technique is incredibly powerful and opens the door for Turbo users to begin to experiment with more complex DOM manipulation in Turbo Stream actions. Our example dispatches an event but we could just as easily play a sound, morph in new content with morphdom, manipulate data attributes, or add or remove CSS classes by defining new custom Turbo Stream actions.&lt;/p&gt;

&lt;p&gt;As you experiment with your own custom actions, you’ll find the &lt;a href=&quot;https://github.com/hotwired/turbo/blob/main/src/core/streams/stream_actions.ts&quot;&gt;Turbo source&lt;/a&gt; helpful for understanding how the existing Turbo Stream actions are implemented, and the &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/454efaa27fc64789fca1ea584a0873c82855ba2c/app/models/turbo/streams/tag_builder.rb&quot;&gt;turbo-rails source&lt;/a&gt; helpful for learning how to build new action helpers.&lt;/p&gt;

&lt;p&gt;Let’s finish up this article by handling invalid form submissions with Turbo Streams.&lt;/p&gt;

&lt;p&gt;First, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; method in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/cards_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@card&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Card&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;card_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@card&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;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;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;card_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@card&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;Card was successfully created.&quot;&lt;/span&gt; &lt;span class=&quot;p&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;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;:show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: :created&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;vi&quot;&gt;@card&lt;/span&gt; &lt;span class=&quot;p&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;turbo_stream&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&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;p&quot;&gt;{&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;p&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;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;@card&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&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;p&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;turbo_stream&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;s1&quot;&gt;&apos;card-form&apos;&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;s1&quot;&gt;&apos;form&apos;&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we updated the unhappy path to render an inline &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream.replace&lt;/code&gt; action. When the card isn’t saved, we re-render the form and replace it inline, leaving the modal open with the errors displayed to the user.&lt;/p&gt;

&lt;p&gt;Before we can test this, we need to add validation to the card model. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/card.rb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Card&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_presence_of&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Open the card modal and submit the form with a blank name input. See that the modal stays open with the errors rendered on the form. Beautiful.&lt;/p&gt;

&lt;p&gt;Great work making it to the end of this tutorial — I hope it has helped to see the changes in Turbo 7.2 in action, and I hope it gets the wheels turning on what you can do with custom Turbo Stream actions.&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Today we built a modal form with Rails, Turbo, and Stimulus, using a Turbo Stream GET request and a custom Turbo Stream action, both of which are new tools unlocked by the release of Turbo 7.2.&lt;/p&gt;

&lt;p&gt;Our implementation of a modal has some room to grow. If you want to stretch these muscles a bit, try adding functionality to clear the modal content each time it closes. Without clearing the content on close, the user will see a brief flash of old content each time the modal is opened. To implement this, you might consider emptying the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-title&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-body&lt;/code&gt; elements each time &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;close&lt;/code&gt; is called in the Stimulus controller. Adding the ability to edit existing cards in the same modal would also be a useful exercise.&lt;/p&gt;

&lt;p&gt;If the world of highly targeted DOM manipulation initiated from the server is new to you, you might find inspiration in &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/&quot;&gt;CableReady&lt;/a&gt;. CableReady provides 30+ &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/reference/operations&quot;&gt;operations&lt;/a&gt; out of the box, everything from simple HTML replacement to adding CSS classes, logging to the console, and dispatching events.&lt;/p&gt;

&lt;p&gt;Custom Turbo Stream actions are a powerful tool, but if you find you need more, CableReady is the place to look.&lt;/p&gt;

&lt;p&gt;That’s all for today — as always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Everyone GET in here! Infinite scroll with Rails, Turbo Streams, and Stimulus</title>
   <link href="https://colby.so/posts/infinite-scroll-with-turbo-streams-and-stimulus"/>
   <updated>2022-09-21T00:00:00+00:00</updated>
   <id>https://colby.so/posts/infinite-scroll-with-turbo-streams-and-stimulus</id>
   <content type="html">&lt;p&gt;If you have worked with Turbo Streams, you have probably run into a frustrating limitation. From the beginning, Turbo Streams were &lt;a href=&quot;https://github.com/hotwired/turbo/pull/52&quot;&gt;designed exclusively&lt;/a&gt; for handling form submissions, and that was the way it worked. If you wanted to respond to a GET request with a Turbo Stream, you couldn’t, without a clunky workaround.&lt;/p&gt;

&lt;p&gt;Until now.&lt;/p&gt;

&lt;p&gt;In Turbo 7.2, you can &lt;a href=&quot;https://github.com/hotwired/turbo/pull/612&quot;&gt;respond to GET requests&lt;/a&gt; with Turbo Streams. No more empty Turbo Frame hacks.&lt;/p&gt;

&lt;p&gt;To demonstrate, we are going to build a simple Rails application that supports infinite scrolling through a paginated resource using Pagy, Turbo Streams, and a tiny Stimulus controller.&lt;/p&gt;

&lt;p&gt;When we are finished, our application will work like this, and we won’t use a single Turbo Frame:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-21-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user scrolling down a web page. As they scroll, new records are appended to the list of records automatically.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Before we begin, you should be comfortable working with Rails and have at least a passing familiarity with Turbo. If you have never used Turbo before, this article will not be the best starting place. This article is an update to &lt;a href=&quot;https://www.colby.so/posts/pagination-and-infinite-scrolling-with-hotwire&quot;&gt;an article&lt;/a&gt; I published earlier this year. Reviewing the previous article will help you understand why GET Turbo Streams are such an exciting change.&lt;/p&gt;

&lt;p&gt;As usual, you can find the completed code for this tutorial &lt;a href=&quot;https://github.com/DavidColby/turbo-stream-pagination/tree/complete&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s get started!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h3 id=&quot;application-setup&quot;&gt;Application setup&lt;/h3&gt;
&lt;p&gt;We will start with a new Rails 7 application, using esbuild and Tailwind. Tailwind is entirely optional in this article, but it will make things look a bit nicer.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new turbo-stream-pagination &lt;span class=&quot;nt&quot;&gt;--javascript&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;esbuild &lt;span class=&quot;nt&quot;&gt;--css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;tailwind
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;turbo-stream-pagination&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Make sure that you are using Turbo 7.2 or greater, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; 1.3 or greater.&lt;/p&gt;

&lt;p&gt;To finish application setup, scaffold up a new resource to paginate. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g scaffold card name:string
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Since we are paginating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cards&lt;/code&gt; resource, jump into the Rails console with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails c&lt;/code&gt; from your terminal and populate some database rows with a script like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;nf&quot;&gt;times&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;n&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Card&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;Grim Patron &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Setup complete, let’s start writing some code!&lt;/p&gt;

&lt;h3 id=&quot;paginating-a-resource-with-pagy&quot;&gt;Paginating a resource with Pagy&lt;/h3&gt;
&lt;p&gt;Before we can add infinite scrolling, we need to add simple pagination to our application. We could build this ourselves, but in this article we will rely on &lt;a href=&quot;https://github.com/ddnexus/pagy&quot;&gt;Pagy&lt;/a&gt;, a battle-tested pagination gem that handles all the tricky parts for us.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle add pagy&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Add Pagy to the backend of the application by updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Backend&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And add Pagy’s frontend helpers to the application by updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/application_helper.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ApplicationHelper&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Frontend&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With Pagy installed and ready to use across the application, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/cards_controller.rb&lt;/code&gt; to paginate records on the index page:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@cards&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;no&quot;&gt;Card&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finish up our traditional pagination implementation by adding a pager UI to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/cards/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-1/2 mx-auto&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Cards&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New card&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_card_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;cards&quot;&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;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;@cards&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;pager&quot;&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 my-8 flex justify-between&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;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;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prev&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;s2&quot;&gt;&quot;&amp;lt; Previous page&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cards_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prev&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;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;text-right&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;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;s2&quot;&gt;&quot;Next page &amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cards_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;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&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Perfect. With that update in place, we can now paginate records on the cards index page. Test this out by starting up your Rails application with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; from the terminal and heading to &lt;a href=&quot;http://localhost:3000/cards&quot;&gt;localhost:3000/cards&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While Pagy does all of the heavy lifting for us with pagination, the approach to pagination with Turbo Streams and Stimulus that we will build in the rest of this article will work with any paging strategy. Pagy is optional.&lt;/p&gt;

&lt;p&gt;Next, let’s switch from traditional paging in our index view to a manual version of infinite scrolling with a “Load More” button powered by a Turbo Stream.&lt;/p&gt;

&lt;h3 id=&quot;manual-load-more-button&quot;&gt;Manual “Load more” button&lt;/h3&gt;
&lt;p&gt;Our goal in this section is to replace the Next and Previous buttons in our pager with a single “Load more” button. When the user clicks to load more cards, we will send a Turbo Stream request to the server, retrieve the next page of results, and append the new cards to the existing list. When the user hits the last page of records, the button will be hidden.&lt;/p&gt;

&lt;p&gt;Let’s start by moving the pager HTML to a partial. Create the new partial from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/cards/_pager.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that partial in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;pager&quot;&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 my-8 flex justify-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;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;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Load more cards&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cards_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &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;nf&quot;&gt;next&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;turbo_stream: &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;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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our pager now has a single “Load more cards” link, pointing to the cards index path and passing in the next page as the page param. Easy.&lt;/p&gt;

&lt;p&gt;The more interesting thing to note is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt; has a new data attribute: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt;. This attribute is required to enable the new Turbo Stream GET behavior that allows us to respond with Turbo Stream actions in a GET request.&lt;/p&gt;

&lt;p&gt;See &lt;a href=&quot;https://github.com/hotwired/turbo/pull/612&quot;&gt;the discussion&lt;/a&gt; on the PR that introduced this new functionality if you are curious why we need to opt-in to this behavior with a data attribute.&lt;/p&gt;

&lt;p&gt;Now update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/cards/index.html.erb&lt;/code&gt; to use the pager partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-1/2 mx-auto&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Cards&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New card&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_card_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;cards&quot;&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;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;@cards&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;pagy: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With the Turbo Stream-enabled paging link in place, our last step is to add a Turbo Stream template for the index page. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/cards/index.turbo_stream.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When turbo-rails &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/lib/turbo/engine.rb#L51&quot;&gt;receives an inbound Turbo Stream request&lt;/a&gt;, it automatically checks for a matching view with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; extension. If a matching view is found, it is used. If no Turbo Stream view is found, we fallback to the normal HTML view.&lt;/p&gt;

&lt;p&gt;Fill in the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream.erb&lt;/code&gt; view to finish the manual infinite scroll feature:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream_action_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;append&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;cards&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;template: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@cards&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sx&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;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream_action_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;replace&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;pager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;template: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;pagy: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sx&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we are sending two Turbo Stream actions to the frontend to be processed by Turbo’s JavaScript. The first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appends&lt;/code&gt; the new set of cards to the existing card list. The second &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replaces&lt;/code&gt; the pager partial with a new version of the partial. Both actions are built with the &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams/action_helper.rb#L14&quot;&gt;turbo_stream_action_tag&lt;/a&gt; helper from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We replace the page to update the next page value in the “Load more” button. When there are no pages left, the replace action will remove the pager component from the page. This need to update the pager with a Turbo Stream action is why we moved the pager to a partial at the beginning of the section.&lt;/p&gt;

&lt;p&gt;Refresh &lt;a href=&quot;http://localhost:3000/cards&quot;&gt;localhost:3000/cards&lt;/a&gt; and try out the new Load more button. If all has gone well, it should work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-21-load-more.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page displaying a list of records. At the bottom of the list is a load more button. The user clicks the button and a new set of records is appended to the existing list of records.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To follow along with what’s happening here, check your Rails server logs. When you click the button to load more records, you will see Rails receives the request as a TURBO_STREAM request, which is what the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; attribute enables for GET links (and forms).&lt;/p&gt;

&lt;p&gt;Try removing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; attribute from the button. If you do, you will see the request revert to a normal HTML request. Without the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; attribute, clicking the link results in a full page turn, replacing the list of records with the next page of records.&lt;/p&gt;

&lt;p&gt;Great work following along so far! The last section of this tutorial will demonstrate automatic infinite scroll with the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver&quot;&gt;IntersectionObserver API&lt;/a&gt; and a small Stimulus controller. To make using the IntersectionObserver API easier, we will add the handy &lt;a href=&quot;https://github.com/stimulus-use/stimulus-use&quot;&gt;stimulus-use&lt;/a&gt; package to our application.&lt;/p&gt;

&lt;h3 id=&quot;infinite-scroll-with-stimulus-controller&quot;&gt;Infinite scroll with Stimulus controller&lt;/h3&gt;
&lt;p&gt;While the Load more button we have now is nice, even better (if your goal is trapping people in a social media net, or following along with this tutorial for a few more minutes) is the list of cards updating automatically as the user scrolls to the end of the list.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;yarn add stimulus-use&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Staying in your terminal, generate the Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g stimulus autoclick&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/autoclick_controller.js&lt;/code&gt; and fill it in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useIntersection&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;stimulus-use&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;nc&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;options&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;threshold&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;nf&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;nf&quot;&gt;useIntersection&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;options&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;appear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entry&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;click&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This controller pulls in &lt;a href=&quot;https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-intersection.md&quot;&gt;useIntersection&lt;/a&gt; from stimulus-use. The appear function is triggered when the element the controller is attached scrolls into view in a user’s browser. appear simply calls click() on the element the controller is attached to, saving the user the effort.&lt;/p&gt;

&lt;p&gt;To use this controller, update the pager partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;pager&quot;&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 my-8 flex justify-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;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;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Load more cards&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cards_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &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;nf&quot;&gt;next&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;turbo_stream: &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;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;autoclick&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;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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To push our load more button down below the fold so autoscroll doesn’t fire 5 times as soon as the page loads, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/cards/_card.html.erb&lt;/code&gt; to add a little extra to each card:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;&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;n&quot;&gt;card&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;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-5&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;strong&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;block font-medium mb-1&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Name:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;card&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;/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;n&quot;&gt;action_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;show&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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Show this card&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;card&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;rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium&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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Edit this card&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_card_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;card&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;rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium&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;hr&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mt-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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The markup here is the default markup you get when you use Tailwind through the &lt;a href=&quot;https://github.com/rails/tailwindcss-rails&quot;&gt;tailwindcss-rails&lt;/a&gt; gem.&lt;/p&gt;

&lt;p&gt;Now head back to &lt;a href=&quot;http://localhost:3000/cards&quot;&gt;localhost:3000/cards&lt;/a&gt; one more time and see that new records are automatically retrieved as you scroll down the page.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-21-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user scrolling down a web page. As they scroll, new records are appended to the list of records automatically.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Thanks for following along with me today, you’ve made it to the finish line!&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Today we used the brand new GET Turbo Stream functionality added in Turbo 7.2 to build manual and automatic “infinite” scroll in a Rails application. The big breakthrough here, and the reason I wrote up this article is the removal of the restriction on using Turbo Streams with GET links and forms.&lt;/p&gt;

&lt;p&gt;In previous versions of Turbo, to accomplish this we had to sneak in Turbo Stream actions inside of “empty” Turbo Frames. This technique worked fine, but it added a layer of indirection that could make following what was happening more difficult, especially for developers that were new to Turbo. Rendering an empty Turbo Frame so it could be used to insert Turbo Stream actions into the page was odd — it worked fine, but it always felt uncomfortable.&lt;/p&gt;

&lt;p&gt;This article demonstrates the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; functionality with pagination, but a similar approach can be used to simplify the implementation of other common web application use cases like modals and drawers and search forms.&lt;/p&gt;

&lt;p&gt;For further reading on the changes in Turbo 7.2, keep an eye on the release notes for &lt;a href=&quot;https://github.com/hotwired/turbo/releases&quot;&gt;Turbo&lt;/a&gt; and &lt;a href=&quot;https://github.com/hotwired/turbo-rails/releases&quot;&gt;turbo-rails&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Interactive charts with Ruby on Rails, StimulusReflex, and ApexCharts</title>
   <link href="https://colby.so/posts/interactive-charts-with-rails-and-stimulusreflex"/>
   <updated>2022-09-19T00:00:00+00:00</updated>
   <id>https://colby.so/posts/interactive-charts-with-rails-and-stimulusreflex</id>
   <content type="html">&lt;p&gt;Every B2B web app eventually gets charts. Users love them, buyers love them, sales teams love them. Look at all this data! Look how pretty it is! Charts!&lt;/p&gt;

&lt;p&gt;I love charts too. So, let’s build a filterable pie chart with Rails and StimulusReflex. Our chart will rely on data from the database, and, because it is 2022, we will be able to update the chart as the user applies a filter without requiring a full page turn.&lt;/p&gt;

&lt;p&gt;To render our charts, we will use ApexCharts, but the technique shown in this article will work fine with any other frontend charting library, I just like ApexCharts.&lt;/p&gt;

&lt;p&gt;When we are finished, we will have a Rails app with a chart that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-19-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page with a select input and a pie chart visible. The user changes options on the select menu and the data in the pie chart updates to match the data for the newly selected option&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(It looks nicer when it isn’t being captured as a low quality gif!)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before beginning, this article assumes that you are comfortable with Ruby on Rails and that you have a passing familiarity with Stimulus and that you have written a Stimulus controller or two before. You won’t need any prior experience with StimulusReflex to follow along with this article.&lt;/p&gt;

&lt;p&gt;As always, you can find the completed code accompanying this article &lt;a href=&quot;https://github.com/DavidColby/stimulus-reflex-charts/tree/charting&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s dive in!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;To skip some uninteresting configuration steps, start today by cloning &lt;a href=&quot;https://github.com/DavidColby/stimulus-reflex-charts&quot;&gt;this repo&lt;/a&gt; to your local machine, working from the main branch. Make sure you have node installed and redis running locally.&lt;/p&gt;

&lt;p&gt;The repo comes with Rails 7 using esbuild via jsbundling-rails to bundle JavaScript, StimulusReflex installed, and two models, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Job&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Applicant&lt;/code&gt;, which will be used to provide data to the fancy chart we are going to build. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Jobs&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Applicants&lt;/code&gt; can’t be interacted with in the UI. A seed file has been provided to build enough test data for our chart to use.&lt;/p&gt;

&lt;p&gt;From your terminal, set everything up with:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle &lt;span class=&quot;nb&quot;&gt;install
&lt;/span&gt;yarn &lt;span class=&quot;nb&quot;&gt;install
&lt;/span&gt;bin/rails dev:cache
bin/rails db:prepare db:seed&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then boot the app and build assets with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bin/dev&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Head to &lt;a href=&quot;localhost:3000&quot;&gt;localhost:3000&lt;/a&gt; and see the beautiful “dashboard”, where the new chart will live by the end of this article.&lt;/p&gt;

&lt;h3 id=&quot;adding-a-static-chart&quot;&gt;Adding a static chart&lt;/h3&gt;
&lt;p&gt;To display our chart, we are going to use the handy &lt;a href=&quot;https://apexcharts.com/&quot;&gt;ApexCharts&lt;/a&gt; JavaScript library. ApexCharts will handle rendering the pie chart and animating the transitions when we apply filters to the chart — we give it the data, it does the rest.&lt;/p&gt;

&lt;p&gt;Install ApexCharts from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;yarn add apexcharts&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To interact with ApexCharts we will use a Stimulus controller. From your terminal again:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g stimulus pie_chart&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Head to the generated Stimulus controller at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/pie_chart_controller.js&lt;/code&gt; and fill it in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&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;./application_controller&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApexCharts&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;apexcharts&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&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;chart&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;labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&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;na&quot;&gt;series&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&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;nf&quot;&gt;initialize&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;chart&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;nc&quot;&gt;ApexCharts&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;chartTarget&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;chartOptions&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;chart&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;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;chartOptions&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;na&quot;&gt;chart&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;type&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;pie&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;na&quot;&gt;height&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;400px&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;na&quot;&gt;width&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;400px&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;na&quot;&gt;series&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;seriesValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&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;labelsValue&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Later in this article, this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PieChartController&lt;/code&gt; will call a server-side reflex, so we extend this controller from the base StimulusReflex &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initialize&lt;/code&gt; method, we render a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApexCharts&lt;/code&gt; chart, passing in the configuration options with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chartOptions&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;More details on the options available here can be found in the &lt;a href=&quot;https://apexcharts.com/docs/chart-types/pie-donut/&quot;&gt;ApexCharts docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;series&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;labels&lt;/code&gt; are set using Stimulus’ &lt;a href=&quot;https://stimulus.hotwired.dev/reference/values&quot;&gt;values&lt;/a&gt;. Stimulus values allow us to read and write HTML data attributes, providing a simple way to pass data from the server to our ApexCharts instance. Let’s connect this controller to the DOM to see how that looks in practice.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/dashboards/index.html.erb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Applicant Tracking System Dashboard&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;pie-chart&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-pie-chart-labels-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;vi&quot;&gt;@labels&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-pie-chart-series-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;vi&quot;&gt;@series&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&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Applicants over time&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;data-pie-chart-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;chart&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-pie-chart-labels-value&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-pie-chart-series-value&lt;/code&gt; to pass in the server generated data, and then render the chart in the empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-pie-chart-target&lt;/code&gt; div.&lt;/p&gt;

&lt;p&gt;Before this will do anything but render an empty chart we need to set the values of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@labels&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@series&lt;/code&gt; in the controller action.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/dashboard_controller.rb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DashboardController&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;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Applicant&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;:job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;status&apos;&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;vi&quot;&gt;@labels&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;keys&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;:humanize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@series&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;values&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Nothing too fancy here, just Ruby. We query the database for all of the applicants grouped by the applicant’s status, which in our case will be either “active” or “inactive”.&lt;/p&gt;

&lt;p&gt;The query returns a hash, with the status values as a the keys and the number of applicants in each status as the values, something like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;active&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&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;s2&quot;&gt;&quot;inactive&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The keys become the labels in our pie chart and the values become the series data in the pie chart.&lt;/p&gt;

&lt;p&gt;With this change in place, refresh the dashboard and you should see the pie chart render, like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-19-static-chart.png&quot; class=&quot;centered&quot; alt=&quot;An image of a pie chart on a web page titled Applicant Tracking System Dashboard&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Your percentage of applicants in each status will probably be different than mine, but you should have a chart!&lt;/p&gt;

&lt;p&gt;Right now the pie chart just shows us the status of all applicants in the database which isn’t very useful. Let’s add a filter to drill down to applicants for a specific job, applying the new results without a page turn.&lt;/p&gt;

&lt;h3 id=&quot;add-job-name-filter-to-chart&quot;&gt;Add job name filter to chart&lt;/h3&gt;
&lt;p&gt;We want users to be able to filter our chart by the name of a job posting. To do this, we will add a select input to the dashboard. When the user changes the value of the select, we will query the database for data specific to the selected job and re-render the chart with updated data.&lt;/p&gt;

&lt;p&gt;To start, let’s add the select input to the dashboard. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/dashboards/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Applicant Tracking System Dashboard&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;pie-chart&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-pie-chart-labels-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;vi&quot;&gt;@labels&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-pie-chart-series-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;vi&quot;&gt;@series&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&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Applicants over time&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;form&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;s2&quot;&gt;&quot;job_id&quot;&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;no&quot;&gt;Job&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;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;:id&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 jobs&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;change-&amp;gt;pie-chart#update&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;/form&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-pie-chart-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;chart&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the addition of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; input and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select_tag&lt;/code&gt; that rolls up all of the jobs in the database and plucks the name and id. In a real application, we might want to be a little more selective in building the option list, but our database only has three jobs so it will be okay.&lt;/p&gt;

&lt;p&gt;Also on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select&lt;/code&gt; is an associated Stimulus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;action&lt;/code&gt;. When the select changes, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pie-chart#update&lt;/code&gt; will be called. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; doesn’t exist in our Stimulus controller yet, so let’s add that now.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/pie_chart_controller.js&lt;/code&gt; add these two new methods to the controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;nx&quot;&gt;event&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;nf&quot;&gt;stimulate&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;PieChart#update&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;event&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;serializeForm&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;nf&quot;&gt;afterUpdate&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;chart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;updateOptions&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;chartOptions&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Hey, StimulusReflex code! &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.stimulate&lt;/code&gt;, a method provided by StimulusReflex that we have access to in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PieChartController&lt;/code&gt; because it extends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As described in the docs, &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/rtfm/reflexes#calling-a-reflex-in-a-stimulus-controller&quot;&gt;stimulate&lt;/a&gt; allows us to call server-side reflexes (in this case, the yet to be created &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PieChartReflex#update&lt;/code&gt; method)  from our JavaScript code. Because the server needs access to the form data from the frontend, we add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;serializeForm&lt;/code&gt; &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/rtfm/working-with-forms#stimulusreflex-form-processing&quot;&gt;option&lt;/a&gt; to our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulate&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;After the reflex runs, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;afterUpdate&lt;/code&gt; tells ApexCharts to re-render our chart with the handy &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updateOptions&lt;/code&gt; &lt;a href=&quot;https://apexcharts.com/docs/methods/#updateOptions&quot;&gt;method&lt;/a&gt; provided by ApexCharts.&lt;/p&gt;

&lt;p&gt;Almost there.&lt;/p&gt;

&lt;p&gt;Before changing the job name select will do anything but generate an error, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PieChart&lt;/code&gt; reflex that our Stimulus controller calls must exist.&lt;/p&gt;

&lt;p&gt;From your terminal, create the reflex:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/reflexes/pie_chart_reflex.rb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;em&gt;(In normal times, StimulusReflex &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/hello-world/quickstart#stimulusreflex-generator&quot;&gt;provides a generator&lt;/a&gt; to create both a new _reflex file and the associated _controller JavaScript file. While they prepare for the official release of the next version the generator rake task is in flux, so we are not using it in this article.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fill that new file in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PieChartReflex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationReflex&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;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;retrieve_data&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;:job_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@series&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;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;keys&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;:humanize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_json&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;retrieve_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Applicant&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;:job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;for_job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;status&apos;&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Accessing the form data from the client side is as easy as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params[:job_id]&lt;/code&gt; because of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;serializeForm&lt;/code&gt; option in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulate&lt;/code&gt; call on the client side. Using the job id, we retrieve updated applicant details from the database and split that data out into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@labels&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@series&lt;/code&gt;  instance variables.&lt;/p&gt;

&lt;p&gt;Here the really important thing to note is that we are setting the same instance variables that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DashboardController&lt;/code&gt; sets. Recall that in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method we only set the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@labels&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@series&lt;/code&gt; if they are not already set:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Applicant&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;:job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;status&apos;&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;vi&quot;&gt;@labels&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;keys&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;:humanize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;vi&quot;&gt;@series&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;values&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When we call a reflex method, StimulusReflex reruns the current page’s controller action and renders that page’s ERB template after processing the reflex, while retaining any instance variables we have set, as described in &lt;a href=&quot;https://lucid.app/lucidchart/e83d2cac-d2b1-4a05-8a2f-d55ea5e40bc9/view?page=0_0#&quot;&gt;this handy diagram&lt;/a&gt; from the StimulusReflex documentation.&lt;/p&gt;

&lt;p&gt;Because we are only updating a piece of the page, we could use a &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/rtfm/morph-modes#selector-morphs&quot;&gt;selector morph&lt;/a&gt; instead of the default &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/rtfm/morph-modes#page-morphs&quot;&gt;page morph&lt;/a&gt;. If we wanted to be really efficient, we could use a CableReady &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/operations/element-mutations#set_dataset_property&quot;&gt;operation&lt;/a&gt; combined with a &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/rtfm/morph-modes#nothing-morphs&quot;&gt;nothing morph&lt;/a&gt; to only update the two data attributes that we care about without touching anything else on the page.&lt;/p&gt;

&lt;p&gt;For our use case, a page morph works fine. Selector and nothing morphs are more precise tools that come in handy as your pages get larger and use cases get more complex. We could also fiddle a bit with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DashboardController&lt;/code&gt;. When StimulusReflex reruns the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method, we make an unnecessary database call, retrieving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; that we will never use. If we wanted to, we could ensure that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method skips the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data&lt;/code&gt; query when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@labels&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@series&lt;/code&gt;  are not set, but that’s an unnecessary optimization for our tutorial.&lt;/p&gt;

&lt;p&gt;After the reflex finishes, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;afterUpdate&lt;/code&gt; &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/rtfm/lifecycle#generic-life-cycle-methods&quot;&gt;life-cycle callback&lt;/a&gt; in the Stimulus controller runs which triggers ApexCharts to animate in the changes to chart.&lt;/p&gt;

&lt;p&gt;One last step before this will work. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;retrieve_data&lt;/code&gt;, we use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for_job&lt;/code&gt; scope that doesn’t exist yet.&lt;/p&gt;

&lt;p&gt;Head over to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/applicant.rb&lt;/code&gt; to add that scope:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:for_job&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;job_id&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;job_id&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;p&quot;&gt;?&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;ss&quot;&gt;job_id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job_id&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;all&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we are all set. Refresh the dashboard, change the job name input and see the chart smoothly animate in the changes to the data.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-09-19-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page with a select input and a pie chart visible. The user changes options on the select menu and the data in the pie chart updates to match the data for the newly selected option&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Incredible stuff.&lt;/p&gt;

&lt;p&gt;Great work following along today, you’ve reached the end of this tutorial!&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Today we worked through building a filterable chart that retrieves new data from the server without requiring a page turn. To do this, we used Ruby on Rails along with Stimulus and StimulusReflex to power the chart interactivity, and ApexCharts to save us from trying to build our own chart renderer in a brief tutorial.&lt;/p&gt;

&lt;p&gt;This is a small, simple example of what you can do with StimulusReflex and CableReady, and hopefully it provides a good idea of the simplicity and flexibility of one of my favorite libraries in Rails-land.&lt;/p&gt;

&lt;p&gt;To learn more about StimulusReflex, &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/&quot;&gt;the docs&lt;/a&gt; are the best place to start. Note that the team is nearing final release of 3.5, a long-in-the-works release that will bring with it support for installing StimulusReflex with a single command.&lt;/p&gt;

&lt;p&gt;Until 3.5 officially releases, installing StimulusReflex into a non-webpacker Rails app requires a bit of manual work. Marco Roth’s &lt;a href=&quot;https://github.com/marcoroth/rails7-stimulus-reflex-esbuild&quot;&gt;Stimulus Reflex esbuild repo&lt;/a&gt; outlines the steps to get up and running manually and will be a great resource until the 3.5 release. Thanks to Marco for providing his example repo, which served as my starting point for this tutorial.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article, you might enjoy my (free) book, &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats&quot;&gt;Hotwiring Rails&lt;/a&gt;, from which is article was adapted. In the book, we build a fully functional Rails 7 application from scratch, focusing on adding interactive features with Turbo, Stimulus, CableReady, StimulusReflex, and friends.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Hotwiring Rails Newsletter - August 2022</title>
   <link href="https://colby.so/posts/hotwiring-rails-august-2022"/>
   <updated>2022-08-01T00:00:00+00:00</updated>
   <id>https://colby.so/posts/hotwiring-rails-august-2022</id>
   <content type="html">&lt;p&gt;This newsletter went out via email to the Hotwiring Rails subscriber list, but I know that not everyone wants another email cluttering up their inbox. For those folks, I’ll always publish the full content of the newletter here too, so you can get all the content with none of the emails.&lt;/p&gt;

&lt;p&gt;Thoughts or feedback on how I can make this newsletter more valuable? Have something you’d like to include in next month’s edition? Send me an &lt;a href=&quot;mailto:david@colby.so?subject=Hotwiring+Rails&quot;&gt;email&lt;/a&gt;, or find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#articles-and-guides&quot;&gt;Articles and guides&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#my-recent-writing&quot;&gt;My recent writing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;articles-and-guides&quot;&gt;Articles and guides&lt;/h2&gt;

&lt;h3 id=&quot;fly-reactive-rails-by-julian-rubisch&quot;&gt;Fly Reactive Rails by Julian Rubisch&lt;/h3&gt;
&lt;p&gt;Julian dives deep into how he deploys reactive Rails applications on Fly.io. Fly takes (a lot) more work to get an application deployed compared to Heroku and Render, but for the right type of application, there are major benefits.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://blog.minthesize.com/fly-reactive-rails&quot;&gt;Read the article on minthesize.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;vite-lizing-rails-by-vladimir-dementyev&quot;&gt;Vite-lizing Rails by Vladimir Dementyev&lt;/h3&gt;
&lt;p&gt;Keeping up with the front end bundling world is exhausting, but Vite is worth taking a look at, especially if you’re considering making a switch from Webpacker to the new cssbundling/jsbundling options added to Rails 7. Vite is a nice alternative to esbuild if you want a more full-featured option with glob imports, HMR, and live reload ready to go (almost) out of the box. You can get there with esbuild, but it requires a bit more work and the developer experience is not quite as nice.&lt;/p&gt;

&lt;p&gt;Vladimir’s write up on migrating a Rails app to Vite is very straightforward and, as a nice bonus, it includes a look at his Docker setup for Vite, if you’re into that sort of thing.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://evilmartians.com/chronicles/vite-lizing-rails-get-live-reload-and-hot-replacement-with-vite-ruby&quot;&gt;Read it on evilmartians.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;using-turbo-streams-with-kredis-by-mansa-keïta&quot;&gt;Using Turbo Streams with Kredis by Mansa Keïta&lt;/h3&gt;
&lt;p&gt;This is a quick, interesting look at using Turbo Stream broadcasts without a backing ActiveRecord model. Kredis + a PORO gets the job done as Mansa builds a simple counter app that stays in sync across browsers, even when updates are triggered from the Rails console.&lt;/p&gt;

&lt;p&gt;My favorite tip here is using Turbo::StreamsChannel to broadcast directly to a channel, which is a nice power move for Streams users.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/mansakondo/using-turbo-streams-with-kredis-3d02&quot;&gt;Read the tutorial on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;booleans-dont-exist-in-ruby-by-mike-burns&quot;&gt;Booleans don’t exist in Ruby by Mike Burns&lt;/h3&gt;
&lt;p&gt;Mike takes us on a fun trip to answer the question of whether or not Ruby has booleans. Does Ruby have booleans? As Mike says, “That’s a question for you and Ruby to answer together.”&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://thoughtbot.com/blog/what-is-a-boolean&quot;&gt;Explore the depths of Ruby’s boolean implementation on thoughtbot.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;video--subdomain-routing-in-rails-by-cj-avila&quot;&gt;Video | Subdomain Routing in Rails by CJ Avila&lt;/h3&gt;
&lt;p&gt;This video from CJ shows you how to use subdomain constraints to handle subdomain routing in Rails. This is an area that can get tricky, especially if you’ve never needed to think much about dealing with subdomains while developing locally. CJ’s thorough look at how it works (and live trouble shooting) is a good introduction to the problem.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=T1pSiWOf0O4&quot;&gt;Watch it on Youtube&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;galaxy-brain-css-tricks-with-hotwire-and-rails-by-matt-swanson&quot;&gt;Galaxy Brain CSS Tricks with Hotwire and Rails by Matt Swanson&lt;/h3&gt;
&lt;p&gt;This article has a few simple, effective tips for leveraging CSS to clean up your Rails code. Each tip is bite sized, general purpose, and easy to apply to your own code. Note that a few are Tailwind specific, but not all. The highlight for me isn’t a CSS tip specifically, but instead a plea to stop interpolating CSS classes in your views by using Rails’ tag and class_name helpers instead.&lt;/p&gt;

&lt;p&gt;5 minutes in this article will probably send you away with something new to add to your bag of tricks.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://boringrails.com/articles/css-tips-and-tricks-hotwire/&quot;&gt;Read it on boringrails.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;video--naming-things-rails-new-by-joel-drapper-and-kasper-timm-hansen&quot;&gt;Video | Naming things: rails new by Joel Drapper and Kasper Timm Hansen&lt;/h3&gt;
&lt;p&gt;Naming Things is a neat new addition to Rails-land from Joel. In the inaugural episode, he pairs with Kasper on designing and building out a bit of an event scheduling application.&lt;/p&gt;

&lt;p&gt;As a viewer, you’re listening in on their pairing session as they think through the design of the application and start piecing together some of the code — I really enjoyed hearing two very talented Rails devs talk through problems together. Getting that kind of view of how other folks work is neat, IMO.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.namingthings.org/p/rails-new&quot;&gt;Watch it on namingthings.org&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/h2&gt;
&lt;p&gt;Turbo has a new 7.2 release in beta, and there are some really big changes that I was not expecting to see get merged. Some highlights:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/hotwired/turbo/pull/612&quot;&gt;Allow Turbo Streams with GET&lt;/a&gt; (with an important &lt;a href=&quot;https://github.com/hotwired/turbo/pull/647&quot;&gt;follow up PR&lt;/a&gt;): This is a huge change that should eliminate the need for clunky, Turbo Frame workarounds for responding to GET requests with a Turbo Stream action. There are immediate, obvious applications here improving the DX of building search interfaces and pagination with Turbo.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/hotwired/turbo/pull/431&quot;&gt;Support custom rendering in turbo events&lt;/a&gt;: This PR came in last year and wasn’t picked up but has gotten a refresh for the 7.2 release. The changes here make it easier to extend Turbo to work with third party libraries like &lt;a href=&quot;https://github.com/patrick-steele-idem/morphdom&quot;&gt;morphdom&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On the other side of the Hotwire stack, Stimulus also has a new release in the works with support for &lt;a href=&quot;https://github.com/hotwired/stimulus/pull/535&quot;&gt;new action options&lt;/a&gt; plus support for &lt;a href=&quot;https://github.com/hotwired/stimulus/pull/567&quot;&gt;custom action options&lt;/a&gt; merged into the main branch.&lt;/p&gt;

&lt;p&gt;I’m excited to see such big changes coming for both Turbo and Stimulus — both projects are growing in popularity and the ongoing development is a great sign of the long term health of the Hotwire stack.&lt;/p&gt;

&lt;h2 id=&quot;my-recent-writing&quot;&gt;My recent writing&lt;/h2&gt;
&lt;p&gt;Along with taking a break from this newsletter, I took a break from blogging for the last few months, but I’m off the break and back to writing. In July, I focused on non-tutorial writing so these two pieces might be a little different than what you’re used to. I’ll be back to tutorial writing this month.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.colby.so/posts/rails-is-back-isnt-enough&quot;&gt;Rails is back isn’t enough&lt;/a&gt;: Some thoughts on the scarcity of junior level positions in certain parts of the Rails world, and what that scarcity means for the health of Rails on very long time scales. Twitter thought this was a Good One.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.colby.so/posts/writing-effective-coding-tutorials&quot;&gt;Writing effective coding tutorials&lt;/a&gt;: I read a lot more articles about Rails than what I include in this newsletter. A lot more. Many of them are great technically, but very hard to get value from.&lt;/p&gt;

&lt;p&gt;In this (extremely niche) article, I compiled a few of the common mistakes I see engineers make when writing coding tutorials, along with simple ways to fix those mistakes and deliver a better end product to readers.&lt;/p&gt;

&lt;h2 id=&quot;the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/h2&gt;
&lt;p&gt;I’m job searching right now, and so I’ve spent a lot of time thinking about (and retelling) the time I spent building the engineering and product org at my last company. I told a very small piece of that story in Rails is back isn’t enough, linked above.&lt;/p&gt;

&lt;p&gt;Expanding a bit on the story of my time at that company: My extremely small team (1 engineer, 0 designers, me as PM + eng in one) was tasked with building an entirely new product adjacent to our existing product. The existing, established product had a long queue of planned work needed to keep our edge in the market. This wasn’t a great plan, in retrospect.&lt;/p&gt;

&lt;p&gt;Taking on an entirely new product without a dedicated team to build it was a bad idea. We needed to choose one product, or we needed to add enough resources to execute well on both products at the same time. Instead, the company chose to try to do both without adding enough resources. As a result, the new product suffered from half-baked ideas and a lack of vision because the business continued to put most of its energy into the existing, established product. That product paid the bills, so the math was easy.&lt;/p&gt;

&lt;p&gt;This seems like a simple lesson in retrospect, but I see these sorts of poor strategic decisions made all the time — chasing new opportunities without consolidating existing wins, spreading focus across multiple initiatives because narrowing focus is scary, or individuals with 100 ideas, each of which gets 1% of their energy. Companies try to implement things like EOS to combat this focus problem, but ultimately it takes courage and conviction to hold the line when facing constant distractions. Organizational management models only get you so far.&lt;/p&gt;

&lt;p&gt;That was a very long introduction to &lt;a href=&quot;https://boz.com/articles/half-staffed&quot;&gt;this article&lt;/a&gt; from Meta’s CTO, Andrew Bosworth, on ruthless prioritization. Aptly titled “Half staffed is unstaffed” it distills many of the thoughts I’ve had on this recently, and is worth a read if you are thinking about how to allocate your resources (personally, or for a team) more effectively.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;At the organizational level the best solution I have found is to always staff from the inside out. You must fully fund the core and if you can’t do that then shut it all down. If you have space beyond that you can continue to fund things fully, one at a time, in accordance with your portfolio of goals. When you can’t fully staff another team then you stop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;https://boz.com/articles/half-staffed&quot;&gt;Read the article on boz.com&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;until-next-time&quot;&gt;Until next time&lt;/h2&gt;

&lt;p&gt;It is nice to be back with all ~500 of you after a couple of months away! In case you missed it, Rails conf talks are up on &lt;a href=&quot;https://www.youtube.com/playlist?list=PLbHJudTY1K0f1WgIbKCc0_M-XMraWwCmk&quot;&gt;YouTube&lt;/a&gt; and there is a new &lt;a href=&quot;https://share.transistor.fm/s/89f58bb8&quot;&gt;Ruby podcast&lt;/a&gt; that you might enjoy.&lt;/p&gt;

&lt;p&gt;If you enjoyed this newsletter, send me a note and let me know or share it with a friend who might enjoy it too.&lt;/p&gt;

&lt;p&gt;PS — I’m actively looking for a new full-time role (remote, in the US).&lt;/p&gt;

&lt;p&gt;I’m most interested in product or engineering leadership roles at early stage companies, but I’ll happily consider IC roles in product or engineering on kind teams doing interesting work. You can reply to this email or find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt; if you know of an opportunity that might be a match.&lt;/p&gt;

&lt;p&gt;You can find my odd work history on &lt;a href=&quot;https://www.linkedin.com/in/davidcolbyatx/&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Rails is back isn't enough</title>
   <link href="https://colby.so/posts/rails-is-back-isnt-enough"/>
   <updated>2022-07-30T00:00:00+00:00</updated>
   <id>https://colby.so/posts/rails-is-back-isnt-enough</id>
   <content type="html">&lt;p&gt;&lt;small&gt;&lt;em&gt;The article below is on a topic that has bothered me for a long time — the lack of opportunity for junior Rails developers — spurred by some recent Twitter energy around Ruby on Rails being “back”. I got mentioned in one of the viral threads about Rails being back which jump started my brain into writing this article, but I don’t write it to admonish folks who are excited about Rails and want to bring in more Rails developers with educational content. I love that work, and appreciate the positive energy, and the commitment to building great content. The Rails is back energy just happened to kick off this train of thought about what the long term outlook of the Rails world is when there are few paths for folks to make their living as junior Rails developers.&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;2022 is an exciting time for Rails developers.&lt;/p&gt;

&lt;p&gt;The release of Rails 7 brought &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire&lt;/a&gt; into the default Rails package along with new &lt;a href=&quot;https://github.com/rails/cssbundling-rails&quot;&gt;asset bundling&lt;/a&gt; &lt;a href=&quot;https://github.com/rails/jsbundling-rails&quot;&gt;solutions&lt;/a&gt; to get applications off of the dreaded Webpacker. For fans of Redis, the new &lt;a href=&quot;https://github.com/rails/kredis&quot;&gt;Kredis gem&lt;/a&gt; is a suggested default for Rails 7 applications, offering tools to make it easier for Rails devs to do fun stuff with Redis.&lt;/p&gt;

&lt;p&gt;Outside of officially endorsed Rails projects, the StimulusReflex team is steadily making improvements to &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;, and the related set of projects that make building modern, reactive applications in Rails easier than ever.&lt;/p&gt;

&lt;p&gt;Despite all of that excitement, folks in the Rails community have gotten used to dealing with “Is Rails dead? Rails is dead.” questions and comments that discourage developers from choosing Rails for their next project.&lt;/p&gt;

&lt;p&gt;“Is Rails dead?” questions are silly if you spend any time keeping up with the Rails ecosystem. Rails isn’t dead, it is alive and kicking, just like it has been for nearly 2 decades. Many billions of dollars have been earned riding the Rails and new businesses continue to be built on the foundation that Rails provides.&lt;/p&gt;

&lt;p&gt;As a reaction to “Is Rails dead?”, Rails enthusiasts are leaning into a “Rails is back!” narrative — we aren’t dead, we’ve got all this cool new stuff to share!&lt;/p&gt;

&lt;p&gt;I don’t think the noise of “Rails is dead!” and “Rails is back” is particularly interesting. Neither narrative adds any value to the conversation. As a Rails enthusiast (and sometime Rails content creator), I love the &lt;em&gt;idea&lt;/em&gt; of “Rails is back”, but I don’t think it gets us where we are trying to go.
&lt;!--more--&gt;
To begin, “Rails is back!” isn’t very helpful because Rails isn’t back. It never left. Its place in the world of web frameworks just changed. No new JavaScript library or gem will fundamentally change Rails’ value proposition or turn back the clock to a time when Rails was the hottest framework on the block. That time has long gone.&lt;/p&gt;

&lt;p&gt;Instead of worrying about Rails coming back, I’m more interested in how to make Rails more accessible to new developers. The long term health of the Rails community relies on attracting and nurturing new Rails enthusiasts, not how loudly we yell about the cool stuff you can build with Hotwire (and I’ve yelled very loud about it myself). I don’t see any evidence that Rails is back for junior developers, and I don’t see much indication that it will be without major philosophical shifts from folks hiring Rails developers.&lt;/p&gt;

&lt;p&gt;There are &lt;a href=&quot;https://www.indeed.com/jobs?q=ruby%20on%20rails%20developer&amp;amp;sc=0kf%3Aexplvl(ENTRY_LEVEL)%3B&quot;&gt;very few options&lt;/a&gt; for junior developers to get their start in Rails. New web developers are likely to have far more opportunities if they learn React and Node to land their first gig. The Rails world is welcoming and kind (&lt;a href=&quot;https://en.wikipedia.org/wiki/Yukihiro_Matsumoto&quot;&gt;MINASWAN&lt;/a&gt; is real, in my experience), but we don’t make it easy for new developers to earn a living. Unless we fix this problem, Rails will eventually lose real momentum — the people leading Rails today won’t be pushing the framework forward in the decades to come.&lt;/p&gt;

&lt;p&gt;Speculation time: I suspect that many companies hiring Rails developers are hiring exclusively for senior level roles because companies using Rails tend to run quite lean.&lt;/p&gt;

&lt;p&gt;At my last company we built a multimillion dollar SaaS business with a literal handful of engineers. When I left after ten years, we had two large, complex B2B products supported by eight full-time engineers and one engineer manager. We had less than five engineers for the first five years of the company’s life. Our competitors, mostly using the latest JavaScript frameworks (one even built their own, for unknowable reasons), had dozens or hundreds of engineers. To keep up, we had to be dramatically more efficient than them.&lt;/p&gt;

&lt;p&gt;Rails let us run that lean, but it came with an obvious tradeoff. When you are running lean, there is very little space to provide a supportive environment for juniors. In my time there, I hired three junior developers — one miraculously figured it out with very little guidance outside of what she could learn by osmosis and became a very talented lead. The other two left because my team didn’t provide the environment they needed to learn and grow at a reasonable pace. After those failures, I did the same thing a lot of engineering leaders have done and stopped hiring juniors. From what I know of other companies in the Rails world, this is a common experience. Hire juniors, fail to support them because there is no time to do so, see the juniors fail, stop hiring juniors.&lt;/p&gt;

&lt;p&gt;Over time this cycle leads to a dearth of junior and entry level positions for Rails developers. And without junior developers, the pipeline of senior talent eventually runs dry. If there are no juniors slots open, where do you expect the seniors to come from?&lt;/p&gt;

&lt;p&gt;Eventually, this has severe consequences for the health of the Rails world, and makes me skeptical of any unqualified claims that Rails is back. If you can’t recruit senior Rails talent to keep up with demand as your product grows, that becomes a business risk. I’ve seen this personally — hiring experienced Rails developers is extremely hard, to the point of being nearly impossible for small companies without large budgets and a well-known brand.&lt;/p&gt;

&lt;p&gt;In the future, Rails is at risk of becoming non-viable for new businesses with big aspirations. Not for technical reasons but because recruiting Rails developers is just too difficult. We aren’t anywhere near that yet, but the talent deficit will eventually lead us there. Responsible founders and CTOs will choose other options because they know they’ll be able to build a high quality team faster.&lt;/p&gt;

&lt;p&gt;In my current job search (&lt;a href=&quot;mailto:david@colby.so&quot;&gt;I’m available&lt;/a&gt; for engineering and product leadership roles, dear reader) I have heard a version of this story multiple times from CTOs who profess a love for Rails but chose another option for their company because they didn’t think they could hire enough Rails developers to keep up with their growth plans. As much as I love Rails and the power that Turbo, Stimulus, and friends provide, cool new toys won’t fix the talent pipeline problem.&lt;/p&gt;

&lt;p&gt;Could the new toys attract new talent to Rails (or bring some developers back to Rails)? Yes, sure. I’ve talked to a number of companies that are reevaluating their options because of the efficiency gains they hope to see from Hotwire; however, those are usually companies that are already using Rails extensively and just want to get away from React on the frontend. That’s not going to move the needle much in the long run.&lt;/p&gt;

&lt;h2 id=&quot;what-do-we-do&quot;&gt;What do we do?&lt;/h2&gt;
&lt;p&gt;I’m not sure. I don’t have a grand idea of how to fix the talent pipeline problem. I’m just a guy that ran a Rails shop for a while and now spends a lot of time thinking about Ruby on Rails. Maybe all of my ideas here are wrong!&lt;/p&gt;

&lt;p&gt;Maybe there isn’t even a talent pipeline problem and I’m just a huge goof that was bad at recruiting and worse at long-term planning. Maybe I’ve had the bad luck to talk exclusively to other companies that are also bad at recruiting and long-term planning.&lt;/p&gt;

&lt;p&gt;Assuming there is a talent pipeline problem, and that the pipeline problem is getting worse, here’s some more speculation and a bit about what might help:&lt;/p&gt;

&lt;p&gt;I think the talent pipeline problem is tightly coupled to what makes Rails so attractive in the first place. The Basecamp team has a huge influence on the Rails world, and they showed us that shockingly small, self-funded companies can build large, complex, innovative products that compete and win against established players. That’s really cool! They have had extraordinary success with a very small team. Emulating that success is a huge driver for many folks building companies on Rails, for good reason.&lt;/p&gt;

&lt;p&gt;But it is easy to move from “we don’t need a big team to get great results” to a related but less defensible idea of staying extremely lean and only hiring when it really hurts. Speaking from experience, if you can do a lot with a little, it starts to feel like your competitive advantage is your tiny, very experienced team. Go far enough down that road and you will find yourself building a company that can’t support and nurture junior talent (and burns out your senior talent).&lt;/p&gt;

&lt;p&gt;With the dangers of the extremely lean ethos in mind, one path to fixing the talent pipeline problem might be reevaluating what building a successful company looks like, and rethinking expectations placed on engineers in Rails shops.&lt;/p&gt;

&lt;p&gt;Can you build a great product and get it to market with a few really talented Rails engineers? Yes, absolutely. At my last company, we were doing millions in revenue (and profit each year) with two (2) full-time engineers and one of those engineers was me, fresh out of a bootcamp. Eventually, that masochistic commitment to being lean put tremendous strain on our engineering team, and I suspect our experience matches any other company that takes the extremely lean, self-funded route. It just starts to hurt too much and you cut everything that isn’t shipping code.&lt;/p&gt;

&lt;p&gt;As you grow, redundancy, sustainability, and reliability (of systems and people) all become more important than continuing to wear the extreme lean badge of honor. Investing in a pipeline of junior talent, giving your senior engineers time to coach and mentor those juniors, and slowing down your cadence to give everyone room to breathe in those new processes likely leads to a more successful company. It certainly leads to a more sustainable pipeline of talent moving up the ranks from junior to senior and beyond.&lt;/p&gt;

&lt;p&gt;A single company rethinking the stay lean or die trying approach won’t fix the problem, but if you are in a position to influence growth plans at your Rails shop, think about what the future looks like on the two paths you can take. On one path, you stay lean, you hire seniors, and you keep struggling to find the 5+ years of Rails experience you require for each new hire. On the other path, you slow down a bit. You hire a mix of juniors and seniors, you invest in those juniors, and you build a pipeline of talented folks at all levels of the engineering org.&lt;/p&gt;

&lt;p&gt;Which path feels better for the health of your business, your team, and the Rails world?&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Writing effective coding tutorials</title>
   <link href="https://colby.so/posts/writing-effective-coding-tutorials"/>
   <updated>2022-07-18T00:00:00+00:00</updated>
   <id>https://colby.so/posts/writing-effective-coding-tutorials</id>
   <content type="html">&lt;p&gt;Writing about what you learn, when done well, is a great way to solidify your own knowledge of a topic, stretch yourself professionally, and establish credibility in your field.&lt;/p&gt;

&lt;p&gt;In 2020, I started writing tutorial-style articles for Ruby on Rails developers. Writing these articles began as a fun distraction from a stressful job, and as a way to reconnect with web development as a discipline as my day job pulled me further away from regularly writing code. From that beginning as a distracting hobby, I grew to have a passion for writing tutorials as a way to share my knowledge with internet strangers.&lt;/p&gt;

&lt;p&gt;Over the last 18 months, my writing has helped me grow my professional network, find new consulting clients, earn a few thousand dollars in (self-published) book sales, and to feel much more confident in my technical skills.&lt;/p&gt;

&lt;p&gt;As I’ve worked to improve my own writing, I have read many, many articles from other developers. Many of those articles have been valuable but I also see very common, correctable mistakes from folks who have knowledge to share but struggle with sharing that knowledge effectively.&lt;/p&gt;

&lt;p&gt;Today, I’m sharing some of the most common mistakes I see tutorial writers make and how to correct those mistakes to write more effective tutorials. I will also share the simple formula I use when writing new tutorials.
&lt;!--more--&gt;
Before we begin, there are a few things to keep in mind.&lt;/p&gt;

&lt;p&gt;I am writing this from the perspective of someone who writes web development tutorial articles. Most of my articles are “How to build X with Y”, almost always with a concrete, usable component of a web application built by the end of the tutorial. If you are already writing successful tutorials and feel confident in your own formula, you may not learn anything groundbreaking by reading this piece.&lt;/p&gt;

&lt;p&gt;The ideas I present in this article should be generally applicable to technical tutorials, but some pieces of advice may not apply directly, depending on what you are writing.&lt;/p&gt;

&lt;p&gt;This article does not discuss the distribution of articles or how to build an audience for your hot new tech blog. Today we are focused on improving the quality of your tutorial content. If you write higher quality content, you may be more likely to get some SEO wins or pick up traction in your Twitter niche, but quality is only a piece of the puzzle and, in my experience, only a small piece most of the time. You will not learn how to strike it rich by writing coding tutorials by reading this article, but hopefully you will learn a few things that help you improve your writing.&lt;/p&gt;

&lt;p&gt;Also note that I am writing about mistakes I see when reading coding tutorials, especially those published on large platforms like dev.to and Medium where many aspiring writers get started. I am drawing from experience to write this article but I will not link to specific articles as examples of these common mistakes. I am not trying to put anyone contributing free writing to the programming community on blast, so you will have to take my word for it when I call something a “common mistake”.&lt;/p&gt;

&lt;p&gt;Let’s dive in.&lt;/p&gt;

&lt;h2 id=&quot;write-for-a-specific-audience&quot;&gt;Write for a specific audience&lt;/h2&gt;
&lt;p&gt;When you publish a tutorial on the internet, you have no control over who will read your article. Anyone interested in the topic can click on the article and start reading. This means that you are likely to find readers of all experience levels and abilities, and, try as you might, you cannot write an article that effectively caters to all levels of experience.&lt;/p&gt;

&lt;p&gt;Despite this, I often see coding tutorials that attempt to write for a huge range of experience levels. This happens most often when the article is demonstrating a very complex interaction requiring strong knowledge in the library or language being used while also spelling out for the reader how to do basic tasks like assigning a variable or opening their terminal.&lt;/p&gt;

&lt;p&gt;Instead of trying to write for all possible levels, decide who you are writing for, tailor the content of the article to that level, and then explicitly state who will get the most out of the article in the introduction. If you are writing about an advanced technique that requires a solid grasp of the tech stack you are writing about, tell the reader that and then skip over detailed instructions and explanations for pieces of the project that your advanced readers should understand. If you are writing for more junior folks, slow down and explain things in more detail, narrow the scope if needed, and keep the code simpler to avoid overwhelming the reader with concepts they may be unfamiliar with.&lt;/p&gt;

&lt;p&gt;Make a decision about who you are writing for, state that decision clearly, and then write each section of the tutorial with that audience in mind.&lt;/p&gt;

&lt;h2 id=&quot;make-a-promise-and-keep-it&quot;&gt;Make a promise and keep it&lt;/h2&gt;
&lt;p&gt;Early in your article, tell the reader what the tutorial will teach them, and then follow through on that commitment.&lt;/p&gt;

&lt;p&gt;After reading the first few paragraphs of the article, your reader should know what you are going to help them build and what tools you will use to build it.&lt;/p&gt;

&lt;p&gt;Your promise should be clear and specific, like this: “In this article, we are going to use Ruby on Rails and Stimulus to build a drag-and-drop interface for reordering items in a list and saving the users changes to the database. We will use Tailwind to make the list look fancy and the drag-and-drop UX will be powered by Dragula.”&lt;/p&gt;

&lt;p&gt;In two sentences, the reader knows exactly what to expect from the rest of the article and they can decide if it is something they are interested in reading.&lt;/p&gt;

&lt;p&gt;Once you have made that commitment to the reader, the rest of the article should follow through on that commitment. If you committed to building a drag-and-drop interface with Rails and Stimulus, that’s what every piece of the article should be about.&lt;/p&gt;

&lt;p&gt;Most commonly, writers miss the mark on this by never making a promise to the reader. They just dive right into the code without making it clear what is being built. In the worst cases, the tutorial does not have an introduction at all, we just dive right into the code with no context.&lt;/p&gt;

&lt;p&gt;Perhaps controversially, I do not think you should rely on the article’s title as the only place you make the article’s goal clear to the reader, especially for more complex tutorials. For the drag-and-drop example, the article title might be “Building a drag-and-drop UI with Rails and Stimulus”. This is a good start, but expanding on that promise early in the body allows us to be more clear about the precise set of tools and techniques used to accomplish the goal. You may find that you can make the promise in the title in some cases, but I find restating the promise in more detail to be more effective at setting the stage for the reader.&lt;/p&gt;

&lt;h2 id=&quot;focus-on-what-youre-teaching&quot;&gt;Focus on what you’re teaching&lt;/h2&gt;
&lt;p&gt;After you make a promise to the reader about what you are teaching them, avoid distracting the reader with unexpected or unnecessary code. They signed up to learn X, not Z and W.&lt;/p&gt;

&lt;p&gt;This means stripping the tutorial down to its bare essentials so that you can start meeting your promise to readers as efficiently as possible. Distractions often creep in to coding tutorials in these forms:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Long and complex setups before you can begin working on whatever you have promised to teach the reader.&lt;/li&gt;
  &lt;li&gt;Adding complexity to the project that does not help the reader learn about what you promised to teach them. This often happens when the writer wants to show off, not teach. Unless the purpose of the article is to teach the reader that fancy new technique you just learned, keep it simple.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful technique to eliminate complicated or long setup steps is to provide a starter repo for readers to clone so they can bypass setup steps that are not part of what you are teaching and dive right into learning. You can see this approach in action in one of my &lt;a href=&quot;https://www.colby.so/posts/user-notifications-with-rails-noticed-and-hotwire&quot;&gt;recent articles&lt;/a&gt; which required a non-trivial amount of setup to make good use of the library the article was focused on.&lt;/p&gt;

&lt;p&gt;A more general technique to keep your readers focused is to read through your first draft and look out for code that could be simplified or that is unrelated to what you are building. Simplify what you can, remove code that is not necessary, and trim explanatory content that is not essential to understanding how the code works.&lt;/p&gt;

&lt;h2 id=&quot;create-visual-checkpoints&quot;&gt;Create visual checkpoints&lt;/h2&gt;
&lt;p&gt;Web development tutorials are almost always focused on building a thing that appears on the screen, but writers commonly forget to show the reader what they’ve built.&lt;/p&gt;

&lt;p&gt;Visuals (images, gifs, and videos) serve two purposes in tutorial articles.&lt;/p&gt;

&lt;p&gt;First, they show the reader what they will build. “A drag-and-drop UI” probably means the same thing to most readers, but adding a gif that shows the drag-and-drop UI in action removes any ambiguity. I like to add an image or gif of the finished product to the introduction of every tutorial I write to make sure the reader knows exactly what we are building.&lt;/p&gt;

&lt;p&gt;Second, visuals are a great way to create checkpoints for the reader as they progress through the article. Working all the way through an article only to reach the end and realize that your code isn’t working is a bummer — help your readers avoid this by pausing after each section to check in on what the code they have written should be doing, and, whenever possible, give them a screenshot or a gif to show it in action.&lt;/p&gt;

&lt;p&gt;In our drag-and-drop example, our checkpoints might break down to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Display a static, unstyled list of items on a page
    &lt;ul&gt;
      &lt;li&gt;Visual support: A screenshot of the list of items in a browser&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Add drag-and-drop functionality to the unstyled front end
    &lt;ul&gt;
      &lt;li&gt;Visual support: A gif of the items being reordered via drag-and-drop&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Persist new order of elements to the database
    &lt;ul&gt;
      &lt;li&gt;Visual support: A gif of items being reordered via drag-and-drop and then refreshing the page to demonstrate the new order persisting&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Styling the list
    &lt;ul&gt;
      &lt;li&gt;Visual support: A gif of drag-and-drop list with updated style and animations in use&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taking the time to create these checkpoints in the tutorial and add a visual for each one is extra work. The payoff is that readers are less likely to get lost as they read your work.&lt;/p&gt;

&lt;p&gt;If you are building something without a visual component, it is still helpful to include checkpoints in your tutorial and to try to get creative with ways to include visuals — screenshots of logs, terminal output, or an API response can all be useful ways to give the user a clear way to confirm they are on the right track as they work.&lt;/p&gt;

&lt;h2 id=&quot;build-incrementally&quot;&gt;Build incrementally&lt;/h2&gt;
&lt;p&gt;Closely related to the concept of checkpoints in the last section is building incrementally. Too often, coding tutorials jump right to the finished product. For very simple articles, there may only be one step, but for almost everything worth writing an article about, there are enough steps to break the article down into pieces that can be built sequentially.&lt;/p&gt;

&lt;p&gt;If you are reading this article, you have almost certainly encountered a “tutorial” that fails to build incrementally. The author promises to show you how to build a drag-and-drop UI, and then the content of the article is just blocks of code with no context or explanation of how the author arrived there. In the worst cases, you do not even get comments embedded in the code blocks, just a bunch of code that does… something. As a reader, you’re left to figure out the steps on your own and to piece together what all the context-free code does. Reading those tutorials is the owl meme, but for code.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-07-18-owl.png&quot; class=&quot;centered&quot; alt=&quot;Step 1, draw some circles. Step 2, draw the rest of the fucking owl&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Breaking your articles and explaining each piece as you go has two major benefits.&lt;/p&gt;

&lt;p&gt;First, it forces you to think more deeply about the necessary components of what you are building (helping you focus your writing) and makes it much easier for readers to follow your thought process.&lt;/p&gt;

&lt;p&gt;Second, breaking your work into coherent, incremental steps helps readers actually learn from your article, rather than just copy and pasting your finished code into their project without understanding what the code does. If you take the time to break your work up you can present the reader with the process you took to arrive at the finished product and you can spend time looking at the interesting decisions you made to build the solution.&lt;/p&gt;

&lt;h2 id=&quot;explain-the-code-at-the-right-level&quot;&gt;Explain the code at the right level&lt;/h2&gt;
&lt;p&gt;The folks who jump right to the end without showing any of the incremental steps tend to miss the mark on this too. A coding tutorial should teach the reader how to reproduce what you have built. To do this, your article should explain what the code you have written does at an appropriate level of detail for your target audience.&lt;/p&gt;

&lt;p&gt;If you are writing for beginners, go into greater detail about concepts and go a bit slower when code gets complex. It is better to explain more than you think you need to than it is to gloss over things that you think might be obvious. Avoid using jargon or terms beginners might not understand, and be generous about linking out to further reading on key concepts they will need to understand.&lt;/p&gt;

&lt;p&gt;If a concept is likely to be too confusing for a beginner to grasp, consider whether you need to discuss that topic at all. Perhaps a better approach is to rewrite the code to use a less advanced technique that still solves the problem.&lt;/p&gt;

&lt;p&gt;If you are writing for a more advanced audience, do not waste time and energy explaining in detail things they should already know. If you aren’t sure if they know it, mention it briefly and link out to documentation that goes deeper into a concept for those that need a refresher.&lt;/p&gt;

&lt;p&gt;If the concept is not core to what you are teaching, feel free to gloss over it and trust that your more advanced audience will be comfortable digging into unknowns.&lt;/p&gt;

&lt;h2 id=&quot;check-your-work&quot;&gt;Check your work&lt;/h2&gt;
&lt;p&gt;This should be obvious, but all too often I work through an article and realize that the author has skipped a step, made a typo, or otherwise presented code that does not actually work.&lt;/p&gt;

&lt;p&gt;Before you publish, read through your article, copy and paste each line, and confirm that it all works as you expect it to. You can’t control readers skipping details, failing to have the right tools installed, or otherwise getting stuck on things because they were in a rush. You can be sure that, when you publish an article, it works like it is supposed to.&lt;/p&gt;

&lt;p&gt;You made the reader a promise, keeping it requires checking your work!&lt;/p&gt;

&lt;h2 id=&quot;a-simple-formula-to-get-started&quot;&gt;A simple formula to get started&lt;/h2&gt;
&lt;p&gt;Every coding tutorial I write follows the same basic formula. The choice to follow this formula allows me to spend more time thinking about the content of each article and less time worrying about how to present the article to readers.&lt;/p&gt;

&lt;p&gt;This formula is intended as an example of what works for me, your mileage may vary. You can see this formula in use in articles like &lt;a href=&quot;https://www.colby.so/posts/toggling-view-layouts-with-kredis-and-rails&quot;&gt;this&lt;/a&gt;, &lt;a href=&quot;https://www.colby.so/posts/turbo-rails-101-todo-list&quot;&gt;this&lt;/a&gt;, and &lt;a href=&quot;https://www.colby.so/posts/pagination-and-infinite-scrolling-with-hotwire&quot;&gt;this&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Tell the reader what you are building in the article, what tools you will use to accomplish the task, and who you are writing for. Include a gif or screenshot of the finished product, when relevant. It usually helps to add a little bit about why the reader might want to read about this topic or why you have chosen to write the article.&lt;/p&gt;

&lt;p&gt;In this section you may also find it useful to note the version of tools you are using (“I’m writing this article using Ruby on Rails 7 and Stimulus 3”) so that readers from the distant future know what to expect. I also like to include a link to a Github repo with the final code available so readers can compare their work to yours if they get stuck.&lt;/p&gt;

&lt;h3 id=&quot;setup-the-application&quot;&gt;Setup the application&lt;/h3&gt;
&lt;p&gt;Any setup steps that need to be done to get the reader ready to start building go here. Include a link to a Github repo if setup is complex so readers can skip this step.&lt;/p&gt;

&lt;h3 id=&quot;build-the-application&quot;&gt;Build the application&lt;/h3&gt;
&lt;p&gt;Break each milestone into a separate section, explain each code block as you go, work in small units, and include visuals frequently to keep the reader on track.&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Recap what the reader learned by following along with your tutorial. Add links to further reading and more resources when appropriate. Thank the reader for reading.&lt;/p&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Writing tutorial articles has been a huge boon to my career. I have grown my skills as a developer and as a writer, built a website that &amp;gt; 0 people visit every day, found new consulting clients, and been able to give back to a community that has given a huge amount to me over the last decade.&lt;/p&gt;

&lt;p&gt;My hope in writing this article is that at least one reader will learn something that makes them more successful as a writer. If that’s you, &lt;a href=&quot;mailto: david@colby.so&quot;&gt;send me a note&lt;/a&gt; and say hello.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Hotwiring Rails Newsletter - April 2022</title>
   <link href="https://colby.so/posts/hotwiring-rails-april-2022"/>
   <updated>2022-04-04T00:00:00+00:00</updated>
   <id>https://colby.so/posts/hotwiring-rails-april-2022</id>
   <content type="html">&lt;p&gt;This newsletter went out via email to the Hotwiring Rails subscriber list, but I know that not everyone wants another email cluttering up their inbox. For those folks, I’ll always publish the full content of the newletter here too, so you can get all the content with none of the emails.&lt;/p&gt;

&lt;p&gt;Thoughts or feedback on how I can make this newsletter more valuable? Have something you’d like to include in next month’s edition? Send me an &lt;a href=&quot;mailto:david@colby.so?subject=Hotwiring+Rails&quot;&gt;email&lt;/a&gt;, or find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#articles-and-guides&quot;&gt;Articles and guides&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#my-recent-writing&quot;&gt;My recent writing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;articles-and-guides&quot;&gt;Articles and guides&lt;/h2&gt;

&lt;h3 id=&quot;distributed-systems-in-rails-series-by-nate-hopkins&quot;&gt;Distributed Systems In Rails series by Nate Hopkins&lt;/h3&gt;

&lt;p&gt;This three part series is a super valuable and practical guide to thinking about and structuring distributed systems in Rails. &lt;a href=&quot;https://dev.to/hopsoft/distributed-systems-with-rails-36nm&quot;&gt;Part one&lt;/a&gt; of the series is theoretical, &lt;a href=&quot;https://dev.to/hopsoft/sophisticated-simple-and-affordable-background-workers-14l4&quot;&gt;parts two&lt;/a&gt; and &lt;a href=&quot;https://dev.to/hopsoft/optimizing-rails-connections-4gkd&quot;&gt;three&lt;/a&gt; offer practical approaches to configuring Sidekiq and database connections properly in production.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/hopsoft/distributed-systems-with-rails-36nm&quot;&gt;Read the series on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-problem-that-viewcomponent-solves-for-me-by-jason-swett&quot;&gt;The Problem That ViewComponent Solves For Me by Jason Swett&lt;/h3&gt;

&lt;p&gt;I’ve spent time exploring &lt;a href=&quot;https://github.com/github/view_component&quot;&gt;ViewComponent&lt;/a&gt; before, but I’ve always struggled to articulate exactly when I should reach for a component — my brain usually just defaults to helpers and then one day I wake up to a mess of helpers.&lt;/p&gt;

&lt;p&gt;In this piece, Jason shares his thought process on why he reaches for ViewComponent in a way that helped me reason more clearly about my own code. No code examples in this one, just thoughts.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.codewithjason.com/the-problem-that-viewcomponent-solves-for-me&quot;&gt;Read the article on codewithjason.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;hotwire-handbook-part-1-by-phil-reynolds&quot;&gt;Hotwire Handbook Part 1 by Phil Reynolds&lt;/h3&gt;

&lt;p&gt;This article is a series of small, well-explained features implemented with Turbo Streams.&lt;/p&gt;

&lt;p&gt;While the official documentation lags behind, these types of practical, code-heavy demos are the best way for new folks to get started.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://philreynolds.dev/posts/2022/hotwire-handbook-part-1&quot;&gt;Read the handbook on philreynolds.dev&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;import-maps-under-the-hood-in-rails-7-by-paweł-dąbrowski&quot;&gt;Import Maps Under The Hood In Rails 7 by Paweł Dąbrowski&lt;/h3&gt;

&lt;p&gt;Like many of the new front-end tools released with Rails 7, importmap-rails is not very well documented. This article from Paweł is a thorough look at what import maps are, how they work, and how to use them in your Rails application.&lt;/p&gt;

&lt;p&gt;Import maps are not right for every use case, but if you want to explore, this is a great starting starting point.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://blog.appsignal.com/2022/03/02/import-maps-under-the-hood-in-rails-7.html&quot;&gt;Read it on appsignal.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;dynamically-re-use-and-lazy-load-pages-using-hotwire&quot;&gt;Dynamically Re-use and Lazy-load Pages Using Hotwire&lt;/h3&gt;

&lt;p&gt;This article describes an interesting technique from the Avo team to re-use views across the application, with a healthy dose of Turbo Frames in the mix. The use case here is a bit niche, but the technique is neat.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://avohq.io/blog/dynamically-re-use-pages-with-hotwire&quot;&gt;Read the article on avohq.com&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/h2&gt;

&lt;p&gt;The first &lt;a href=&quot;https://www.ruby-lang.org/en/news/2022/04/03/ruby-3-2-0-preview1-released&quot;&gt;Ruby 3.2 preview&lt;/a&gt; is out and the headline feature is WASM support. Smart and cool folks will undoubtedly find fun ways to use it, once they’ve had time to explore.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/stimulusreflex/cable_ready/releases/tag/v5.0.0.pre9&quot;&gt;CableReady&lt;/a&gt; and &lt;a href=&quot;https://github.com/stimulusreflex/stimulus_reflex/releases/tag/v3.5.0.pre9&quot;&gt;StimulusReflex&lt;/a&gt; both got new pre-release versions, inching closer to major version bumps that have been in the works for many months. The highlight in both new pre-releases is &lt;a href=&quot;https://github.com/stimulusreflex/cable_ready/pull/191&quot;&gt;shipping the package’s JavaScript&lt;/a&gt; along with the Ruby gem, making it much easier to use CableReady and StimulusReflex with importmap-rails.&lt;/p&gt;

&lt;p&gt;In Turbo-land, a &lt;a href=&quot;https://github.com/hotwired/turbo/pull/541&quot;&gt;proposal&lt;/a&gt; to add a new Stream actions for adding and removing classes from elements has been open since late February — the PR addresses a big limitation of Turbo Streams. Given the philosophy of the project, I will be surprised if this proposal is accepted, but if it is it will be a nice upgrade.&lt;/p&gt;

&lt;p&gt;More likely to be accepted is a &lt;a href=&quot;https://github.com/hotwired/turbo/pull/552&quot;&gt;PR adding support for preloading content with Turbo&lt;/a&gt;, improving on limitations of the native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rel=prefetch&lt;/code&gt; behavior which can be tricky to use for dynamic content.&lt;/p&gt;

&lt;p&gt;Finally, an important Turbo &lt;a href=&quot;https://github.com/hotwired/turbo/pull/563&quot;&gt;bug fix PR&lt;/a&gt; was opened, and hopefully will be merged soon. The bug fixed by this PR can be a blocker for migrating cleanly to Turbo, and the double &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:load&lt;/code&gt; event firing can cause all sorts of hard to diagnose problems.&lt;/p&gt;

&lt;h2 id=&quot;my-recent-writing&quot;&gt;My recent writing&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://www.colby.so/posts/toggling-view-layouts-with-kredis-and-rails&quot;&gt;Toggling view layouts with Kredis, Turbo Frames, and Rails&lt;/a&gt; is a quick look at using the very fun Kredis gem to implement a common UX element.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.colby.so/posts/user-notifications-with-rails-noticed-and-hotwire&quot;&gt;User notifications with Rails, Noticed, and Hotwire&lt;/a&gt; offers a quick introduction to Noticed, and demonstrates using Noticed and Turbo Streams to build real-time in-app notifications with minimal effort. Stay tuned for another article demonstrating the same approach with CableReady soon.&lt;/p&gt;

&lt;p&gt;Finally, if reading about non-Rails topics is your thing, I wrote &lt;a href=&quot;https://www.colby.so/posts/publishing-on-gumroad-lessons-learned&quot;&gt;a piece&lt;/a&gt; describing the process of writing and publishing &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats&quot;&gt;my book&lt;/a&gt; on Gumroad and the results of the launch.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Lessons learned from publishing on Gumroad</title>
   <link href="https://colby.so/posts/publishing-on-gumroad-lessons-learned"/>
   <updated>2022-03-24T00:00:00+00:00</updated>
   <id>https://colby.so/posts/publishing-on-gumroad-lessons-learned</id>
   <content type="html">&lt;p&gt;&lt;em&gt;(This post was originally published on &lt;a href=&quot;https://www.indiehackers.com/post/published-a-software-engineering-book-on-gumroad-lessons-from-one-month-post-launch-db0021a1a9&quot;&gt;Indie Hackers&lt;/a&gt;, but I thought some of my regular blog readers might find it interesting, so I’m publishing it here, with light edits.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One month ago I published my first &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats&quot;&gt;ebook&lt;/a&gt; on Gumroad. I went into the process with absolutely no idea what I was doing, and no real idea of what to expect. I wanted to see if I could write and sell a book, so I struck out with that goal in mind and winged it all the way to the finish line.&lt;/p&gt;

&lt;p&gt;Now that I am through the writing and publishing process, I thought others who are considering writing an ebook (especially a technical book like mine), might find value in hearing about my process along with what went well and what could have gone better.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;background&quot;&gt;Background&lt;/h2&gt;
&lt;p&gt;I started consistently publishing Ruby on Rails-focused programming tutorials on this blog in June of 2021. I was fortunate that my articles picked up some traction organically, primarily through high rankings for a couple of articles with very low competition, popular terms for folks looking for information on new pieces of the Rails stack.&lt;/p&gt;

&lt;p&gt;From June to December, traffic grew from &amp;lt; 1,000 visitors in June 2021 to &amp;gt; 5,000 visitors in December of 2021. Most of this growth came from Google search traffic, with Twitter and Reddit making up the bulk of the rest.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/plausible.png&quot; class=&quot;centered&quot; alt=&quot;Plausible analytics screenshot showing visitor growth from June 1 to December 31&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In addition to the organic search traffic my tutorials attracted, my work was shared regularly on social media (especially Twitter and Rails-focused subreddits) when the niche technology I write about was mentioned.&lt;/p&gt;

&lt;p&gt;I had some nice traction and some clear indicators that people were finding my work useful, and I knew that I wanted to write something more substantial than the quick, surface-level content that can fit into a single tutorial.&lt;/p&gt;

&lt;p&gt;So, in November I &lt;a href=&quot;https://twitter.com/davidcolbyatx/status/1458171481029021704&quot;&gt;tweeted&lt;/a&gt; a rough chapter list to see if there was any interest in a longer form version of my writing and it went about as viral as any tweet can with my tiny Twitter presence (maybe 200 people at the time of the tweet). I took that as another good sign and got to work.&lt;/p&gt;

&lt;h2 id=&quot;writing-process-and-pre-launch&quot;&gt;Writing process and pre-launch&lt;/h2&gt;
&lt;p&gt;Since the book is a step-by-step, line-by-line walkthrough of building a Ruby on Rails application, my time “writing the book” was split in half. I first wrote all of the code for the application, taking detailed notes of each change and splitting the code into what felt like coherent “chapters”. I did not write any prose during the first phase — my chapter outlines were code blocks and file names.&lt;/p&gt;

&lt;p&gt;Once I had the application built and each code change captured in my rough outline, I went back through my notes and stepped through each change, writing rough explanations for each block and testing the code for completeness.&lt;/p&gt;

&lt;p&gt;After the code and rough outline was assembled, I stepped through each rough chapter and started writing detailed prose and telling the story of each chapter. This process of build &amp;gt; code notes &amp;gt; prose was basically the same as my process for the tutorial blogs I’d been writing for months, so while it was daunting to write 11 coherent, sequential chapters, it helped that the production process felt familiar.&lt;/p&gt;

&lt;p&gt;After the detailed prose writing was finished, I went through several rounds of copy editing and polishing, along with 3 full run-throughs of each line of code to ensure accuracy and consistency. During this pass, I also captured all of the gifs and images I needed for each chapter to give readers landmarks to compare their progress to the book.&lt;/p&gt;

&lt;p&gt;Once the polishing rounds were past, I sent out the book to beta readers (see below for details) and then spent a month waiting for feedback/incorporating feedback from beta readers.&lt;/p&gt;

&lt;p&gt;All told, I spent approximately 150 hours coding, writing, and editing the content of the book before launch. Of that time, roughly 60 hours was coding, and the rest was writing and editing.&lt;/p&gt;

&lt;h3 id=&quot;beta-readers&quot;&gt;Beta readers&lt;/h3&gt;
&lt;p&gt;In early January, I wanted to give myself a clear deadline to finish the beta draft, so I sent out &lt;a href=&quot;https://twitter.com/davidcolbyatx/status/1478859034782486538&quot;&gt;another tweet&lt;/a&gt; asking for early readers.&lt;/p&gt;

&lt;p&gt;My idea here was to both get feedback on whether the content was useful/confusing/interesting/awful from strangers and to have an opportunity to test out the web application that I built to host my book (see below for details on that).&lt;/p&gt;

&lt;p&gt;Approximately 30 people reached out expressing interest in being beta readers (this felt like a great number given my limited social reach) and I selected 10 to send the book to. I had never run a beta reader program before and had no idea what to expect, but I decided keeping the list of beta readers relatively short would be easier to manage and would keep me from being overwhelmed with feedback.&lt;/p&gt;

&lt;p&gt;I eventually received feedback from 5 of the 10 beta readers, 3 sent very detailed feedback and 2 sent more general feedback, all of which was useful. The detailed, chapter-by-chapter feedback was most valuable, and I think I was quite fortunate to get as much detailed feedback as I did.&lt;/p&gt;

&lt;p&gt;In retrospect, I should have done more to vet my list of beta readers — without getting lucky with a few very kind folks I may have been left with no early feedback on the book because some of the people I selected were not ready for the commitment required to read and offer feedback on the book.&lt;/p&gt;

&lt;h3 id=&quot;publishing-the-book-as-an-app&quot;&gt;Publishing the book as an app&lt;/h3&gt;
&lt;p&gt;I have been coding for a long time, and I have bought quite a few programming ebooks (and plenty of physical programming books) over the years. When I started writing, I assumed that I would publish my book as a PDF, with basic syntax highlighting and otherwise kind of a meh experience. This is how every programming book I’ve read over the years was packaged, so that’s what I went in thinking about.&lt;/p&gt;

&lt;p&gt;As I converted my content to PDF, I was very unhappy with the reading experience. I write long, step-by-step code tutorials, which naturally means my readers are looking at a lot of code, and copying/pasting much of it into their editors as they follow along with the tutorial. On my blog, readers can click a “Copy” button to copy and paste each code snippet, which makes a difference when working with dozens of code snippets in a tutorial.&lt;/p&gt;

&lt;p&gt;I also rely heavily on gifs to provide visual check-ins on what the application looks like throughout the book and how the elements we build work — click this, this happens, click that, that happens. These visual elements make long coding tutorials much more consumable.&lt;/p&gt;

&lt;p&gt;Both copy/pasting with a click and embedding gifs proved to be extremely difficult to accommodate when publishing the book as a PDF.&lt;/p&gt;

&lt;p&gt;I started having nightmares about finding typos the day after launch that day one purchasers would all see forever, and worrying about how to distribute major book revisions in a coherent way or otherwise improve the content of the book after launch in a way that ensured readers would be able to find it.&lt;/p&gt;

&lt;p&gt;All of this weighed on me for weeks, and I ultimately decided the solution was to build a web app to serve my book as basically a fancy blog with user authentication in front of it. This is the silly engineer thing to do – build a whole dang web app to serve some static text content to readers, but that’s the path I chose.&lt;/p&gt;

&lt;p&gt;Ultimately, building the simplest version of the book-as-web-app took ~15 hours to code and deploy to production. Much less than writing the book, but kind of a painful distraction while I was in the polishing phase and finishing the app was a blocker to delivering the book to beta readers for a few days.&lt;/p&gt;

&lt;p&gt;I’m now on the hook for ongoing maintenance of the book’s web app, which has been minimal so far, but is something I have to be mindful of as long as the book is relevant.&lt;/p&gt;

&lt;p&gt;Fortunately, readers have been happy with the delivery method of the book — so much so that I’m working now on &lt;a href=&quot;https://www.scriptive.io&quot;&gt;building a SaaS&lt;/a&gt; to help other technical writers publish their books to the web without having to code a reading app from scratch like I did.&lt;/p&gt;

&lt;h2 id=&quot;launching-and-marketing-the-book&quot;&gt;Launching and marketing the book&lt;/h2&gt;
&lt;p&gt;My very sophisticated marketing plan for the book consisted of a &lt;a href=&quot;https://twitter.com/davidcolbyatx/status/1496555223837466624&quot;&gt;tweet thread&lt;/a&gt; drafted up the night before launch, along with a 20% discount off the list price during the first ~week after launch.&lt;/p&gt;

&lt;p&gt;I sent out the thread on February 23rd with no idea what would happen.&lt;/p&gt;

&lt;p&gt;My realistic goal for launch was to sell one copy of the book. My “I think I can do this goal” was to sell 20 copies of the book, and my “there’s no way I’ll hit this” goal was to sell 50 copies of the book in the launch period.&lt;/p&gt;

&lt;p&gt;My sales chart for the launch week looked like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/launch-day.png&quot; class=&quot;centered&quot; alt=&quot;Gumroad chart showing 58 sales from February 22nd to March 3rd&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I hit all of my goals, which was a huge surprise.&lt;/p&gt;

&lt;p&gt;A few things that worked during the launch week:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The initial tweet thread garnered a lot of attention and retweets from folks with much larger followings. The trust I’ve built up with my free writing gave me enough credibility for others to boost my paid work&lt;/li&gt;
  &lt;li&gt;I sent one more tweet on March 2, one day before the discount ended letting folks know that the discount was ending and that I was one sale away from my stretch goal (which was true)&lt;/li&gt;
  &lt;li&gt;I curate a monthly newsletter for Rails developers and sent out March’s edition on March 1 with a link to buy the book and a note about the discounted pricing&lt;/li&gt;
  &lt;li&gt;I added a share link inside the book to make it easy for folks enjoying the book to share it with others&lt;/li&gt;
  &lt;li&gt;I got one sale from a free listing on dev.to, which took ~30 seconds to create. Every little bit helps.&lt;/li&gt;
  &lt;li&gt;I offered purchasing power parity pricing discounts and about five people reached out to get a discount based on where they live. I used a spreadsheet I found online to calculate discounts, which worked fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of my sales but one came from Twitter and direct links:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/launch-referrals.png&quot; class=&quot;centered&quot; alt=&quot;Gumroad table showing referrals and sales from various sources, with Twitter and direct making up  57 of 58 sales&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As you can see from the charts, after the initial burst of sales from the book launch tweet, sales dropped to basically zero until I started talking about the book again in my newsletter and on Twitter at the end of the discount period.&lt;/p&gt;

&lt;p&gt;Unless I’m actively promoting the book, sales will be very slow!&lt;/p&gt;

&lt;h3 id=&quot;post-launch-period&quot;&gt;Post-launch period&lt;/h3&gt;
&lt;p&gt;In the three weeks since the discounted launch period ended, sales look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/post-launch.png&quot; class=&quot;centered&quot; alt=&quot;Gumroad chart showing 5 total sales over three weeks after March 3rd&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Five total sales, with no more than two in any given day. The two sales in one day came on a day when I published a blog that included a brief blurb about my book at the end — probably not a coincidence.&lt;/p&gt;

&lt;p&gt;In this time period I have also had one buyer ask for a refund, which I immediately granted and then helped the person work through the setup issues they had in their environment that made them unable to get value from the book. I wish I were at zero refunds, but 1 out of 63 feels okay.&lt;/p&gt;

&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons learned&lt;/h2&gt;
&lt;p&gt;Ultimately, I classify the launch of my book as a success based on the original goals I set for myself.&lt;/p&gt;

&lt;p&gt;Writing a book was not very profitable on a per hour rate (nearly 200 hours for $3,000 is not a great return compared to my hourly contracting rate!) but I greatly enjoyed the work and I knew from the outset that this work was an investment in myself, rather than for immediate financial gain.&lt;/p&gt;

&lt;p&gt;I learned a lot, improved as a writer, built a fun web app, created a product that a few people wanted to pay for, and earned my first real dollars on the internet, which feels like a nice milestone.&lt;/p&gt;

&lt;p&gt;If I were going back to do the process over, a few things I would spend more time on:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Working more on marketing. While I never planned to earn a bunch of money from the book, I know that I could have gotten more from the book’s launch with more effort put into promoting the book.&lt;/li&gt;
  &lt;li&gt;Building a system to ask for reviews. I have zero reviews or testimonials on the book right now, which hurts my ability to sell it to people that do not know me. The price tag is pretty high at $70, which is a big investment to make without more social proof. Having a process in place to follow up with buyers and ask them for feedback and reviews would have helped keep sales up after the initial launch excitement died down.&lt;/li&gt;
  &lt;li&gt;Up front planning. Writing a book is hard, even when the book is really just a very, very long tutorial. I struggled to keep my ideas clear and ended up scrapping code and spinning my wheels on unimportant parts of the book because I did not go in with a clear enough plan for every stage of the application. A smoother writing process would have meant an earlier launch and a less stressful writing and editing experience.&lt;/li&gt;
&lt;/ul&gt;

</content>
 </entry>
 
 <entry>
   <title>User notifications with Rails, Noticed, and Hotwire</title>
   <link href="https://colby.so/posts/user-notifications-with-rails-noticed-and-hotwire"/>
   <updated>2022-03-21T00:00:00+00:00</updated>
   <id>https://colby.so/posts/user-notifications-with-rails-noticed-and-hotwire</id>
   <content type="html">&lt;p&gt;A nearly-universal need in web applications is user notifications. An event happens in the application that the user cares about, and you inform the user of the event. A common example is in applications that have a commenting system — when a user mentions another user in a comment, the application notifies the mentioned user of that comment through email.&lt;/p&gt;

&lt;p&gt;The channel(s) used to notify users of new important events depends on the application but the path very often includes an in-app notification widget that shows the user their latest notifications. Other common options include emails, text messages, Slack, and Discord.&lt;/p&gt;

&lt;p&gt;Rails developers that need to add a notification system to their application often turn to &lt;a href=&quot;https://github.com/excid3/noticed&quot;&gt;Noticed&lt;/a&gt;. Noticed is a gem that makes it easy to add new, multi-channel notifications to Rails applications.&lt;/p&gt;

&lt;p&gt;Today, we are going to take a look at how Noticed works by using Noticed to implement in-app user notifications. We will send those notifications to logged in users in real-time with &lt;a href=&quot;https://turbo.hotwired.dev/reference/streams&quot;&gt;Turbo Streams&lt;/a&gt; and, for extra fun, we will load user notifications inside of a &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames&quot;&gt;Turbo Frame&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When we are finished, our application will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-03-21-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of two web browsers open side by side. In one, a user fills out a message into a web form and submits it. In the other, the message the user wrote appears under a Notifications heading after the form is submitted.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Before we begin, this tutorial assumes that you are comfortable building simple Ruby on Rails applications independently. No prior knowledge of Turbo or Noticed is required.&lt;/p&gt;

&lt;p&gt;Let’s dive in!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application setup&lt;/h2&gt;
&lt;p&gt;To follow along with this tutorial, start by cloning &lt;a href=&quot;https://github.com/DavidColby/user-notices&quot;&gt;this repository from Github&lt;/a&gt; and then set it up:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;user-notices
bin/setup&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The starter repo contains a Rails 7 application with Turbo, Tailwind, and Devise ready to go. The starter repo uses Ruby &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3.0.2&lt;/code&gt;, but everything in this tutorial will work fine with Ruby &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.7&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3.1&lt;/code&gt;, if you prefer.&lt;/p&gt;

&lt;p&gt;If you want to work from your own application instead of cloning the starter, you will need a Rails 7 application with Turbo installed and an authentication system built around a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;When you’re ready to start building, start the server and build Tailwind’s css with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;noticed-setup&quot;&gt;Noticed setup&lt;/h2&gt;
&lt;p&gt;Our starter application comes with a Devise-powered user model and the root path set to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dashboard#show&lt;/code&gt; action which just contains links to sign in or sign out for now. Before diving in to the code, create at least one user through the form at &lt;a href=&quot;http://localhost:3000/users/sign_up&quot;&gt;http://localhost:3000/users/sign_up&lt;/a&gt; so that you can login and test notifications later in this tutorial.&lt;/p&gt;

&lt;p&gt;Eventually, our users will be able to create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Messages&lt;/code&gt; for other users in the application. Each time a message is a created, a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Notification&lt;/code&gt; will be created, and the user the message is for will see that notification on their dashboard.&lt;/p&gt;

&lt;p&gt;Before any of that can happen, we need to add Noticed to our application and scaffold a Message resource. Start by adding Noticed, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle add noticed
rails generate noticed:model
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;These commands are straight from the Noticed &lt;a href=&quot;https://github.com/excid3/noticed#-installation&quot;&gt;installation docs&lt;/a&gt;. If your Rails app is running, be sure to restart it after adding the Noticed gem to your Gemfile with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle add&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/user.rb&lt;/code&gt; to associate notifications with users:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&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;devise&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:database_authenticatable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:registerable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;ss&quot;&gt;:recoverable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:rememberable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:validatable&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:notifications&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :recipient&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notifications&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;has_many&lt;/code&gt; association, as directed by the Noticed setup script.&lt;/p&gt;

&lt;p&gt;Now our users can receive notifications, but we don’t have anything useful to notify them about. We will fix that by scaffolding a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Message&lt;/code&gt; resource. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g scaffold message content:text user:references
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Thanks to the magic of Rails, the scaffold generator gives us almost everything we need to start creating messages and associating them with users. Because we are using Tailwind via the &lt;a href=&quot;https://github.com/rails/tailwindcss-rails&quot;&gt;tailwindcss-rails&lt;/a&gt; gem, the scaffold generator also includes some nice looking base styles too.&lt;/p&gt;

&lt;p&gt;After the generator runs, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; model again at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/user.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&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;devise&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:database_authenticatable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:registerable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;ss&quot;&gt;:recoverable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:rememberable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:validatable&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:messages&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:notifications&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :recipient&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;has_many :messages&lt;/code&gt; to set up the other side of the messages relationship.&lt;/p&gt;

&lt;p&gt;For convenience, we can also add a link to the messages index page in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/dashboard/show.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;user_signed_in?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    Signed in as &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_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;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;Sign out&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;destroy_user_session_path&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text-blue-500&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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;All messages&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;messages_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;text-blue-500&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;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;Sign in&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_user_session_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;text-blue-500&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then make a small adjustment to the messages form so we don’t have to memorize user ids:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &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;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contents&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;k&quot;&gt;if&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;nf&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error_explanation&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3&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&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluralize&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;nf&quot;&gt;errors&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error&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; prohibited this message from being saved:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&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;nf&quot;&gt;errors&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;error&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;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-5&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_area&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;rows: &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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full&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;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;my-5&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:user_id&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;form&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;ss&quot;&gt;:user_id&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;no&quot;&gt;User&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;pluck&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;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;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full&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;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&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With these changes in place, head to &lt;a href=&quot;http://localhost:3000/messages&quot;&gt;http://localhost:3000/messages&lt;/a&gt; and create a couple of messages to ensure that creating messages works as expected.&lt;/p&gt;

&lt;p&gt;Now that we have Noticed installed and messages ready to go, next up we will send users notifications when a new message is created.&lt;/p&gt;

&lt;h2 id=&quot;message-notifications&quot;&gt;Message notifications&lt;/h2&gt;
&lt;p&gt;Our goal in this section is to create and display notifications to logged in users on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dashboard#show&lt;/code&gt; page.&lt;/p&gt;

&lt;p&gt;Step one is to add a new notification, using the the generator built-in to Noticed. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails generate noticed:notification MessageNotification&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This generator creates a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageNotification&lt;/code&gt; class at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/notifications/message_notification.rb&lt;/code&gt;. Head there next and make a few small updates:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MessageNotification&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Noticed&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;deliver_by&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:database&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:message&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;message&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;:message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content&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;url&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;message_path&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;: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;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deliver_by :database&lt;/code&gt; stores the newly created notification in the database, which will be important for Turbo Stream broadcasts later. If we wanted to send the notification by email too, we could add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deliver_by :email, mailer: SomeMailer&lt;/code&gt;, as described in the Noticed &lt;a href=&quot;https://github.com/excid3/noticed/blob/master/docs/delivery_methods/email.md&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;param :message&lt;/code&gt; serializes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message&lt;/code&gt; object and stores it with the notification record in the database. We use that serialized &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;url&lt;/code&gt; methods. Serializing objects into params in this manner makes it easy to access records related to the notification without managing complex references — just dump the record(s) you need into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; and profit.&lt;/p&gt;

&lt;p&gt;We can now use our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageNotification&lt;/code&gt; to send notifications to users when a new message is created. Next we need to put the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageNotification&lt;/code&gt; class into use. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/message.rb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Message&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_noticed_notifications&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;after_create_commit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:notify_user&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;notify_user&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;MessageNotification&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;message: &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;deliver_later&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Each time a message is created (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_create_commit&lt;/code&gt;), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notify_user&lt;/code&gt; runs and creates a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageNotification&lt;/code&gt;, serializing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message&lt;/code&gt; object and delivering the message to the message’s user. We also add &lt;a href=&quot;https://github.com/excid3/noticed#associating-notifications&quot;&gt;has_noticed_notifications&lt;/a&gt; to ensure that when a message is destroyed, any related notifications are destroyed too.&lt;/p&gt;

&lt;p&gt;With those small changes, we now have a database-backed notification system up and running. Neat!&lt;/p&gt;

&lt;p&gt;We have notifications in the database now but there is no way for users to see those notifications anywhere, which is not very useful. Next up, we will create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Notifications&lt;/code&gt; controller to display notifications to users.&lt;/p&gt;

&lt;p&gt;From your terminal, generate the controller and a partial to render each notification:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g controller Notifications index
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/notifications/_notification.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/routes.rb&lt;/code&gt; and add a notifications path helper:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;:notifications&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &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;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:messages&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;devise_for&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:users&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;dashboard/show&apos;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dashboard#show&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NotificationsController&lt;/code&gt; at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/notifications_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NotificationsController&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;Notification&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;recipient: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we scope &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notifications&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt;, so users only see their own notifications when logged in to the application.&lt;/p&gt;

&lt;p&gt;Update the new Notifications index view at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/notifications/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;notifications&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;nt&quot;&gt;&amp;lt;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Notifications&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&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;@notifications&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; wrapping the list of notifications. Our plan is to render the list of notifications on the dashboard show page — we will use Turbo Frame’s &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#eager-loaded-frame&quot;&gt;eager-loading functionality&lt;/a&gt; to load the content of the notifications index page on the dashboard show page.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render @notifications&lt;/code&gt; relies on Rails’ &lt;a href=&quot;https://guides.rubyonrails.org/v7.0/layouts_and_rendering.html#rendering-collections&quot;&gt;collection rendering&lt;/a&gt; to render each notification. Before this will work, we need to fill in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/notifications/_notification.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;li&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-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;notification&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_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;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;flex justify-between mt-1 text-gray-500 text-sm space-x-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;p&amp;gt;&lt;/span&gt;
        Received on &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&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;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_date&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&amp;gt;&lt;/span&gt;
        Status: &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&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;read?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Read&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Unread&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;/p&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;/li&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_notification&lt;/code&gt; from Noticed to access the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message&lt;/code&gt; method we added in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageNotification&lt;/code&gt; earlier, and use the built-in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read?&lt;/code&gt; method from Noticed to check if the user has read the notification or not.&lt;/p&gt;

&lt;p&gt;One last step here before users can see notifications on the dashboard. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/dashboard/show.html.erb&lt;/code&gt; and update it to add an eager-loaded Turbo Frame for logged in users:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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&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;n&quot;&gt;user_signed_in?&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;
      Signed in as &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_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;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;Sign out&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;destroy_user_session_path&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text-blue-500&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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;All messages&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;messages_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;text-blue-500&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;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;s2&quot;&gt;&quot;notifications&quot;&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;notifications_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;k&quot;&gt;else&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;s2&quot;&gt;&quot;Sign in&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_user_session_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;text-blue-500&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; has an id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notifications&lt;/code&gt;, which matches the Turbo Frame rendered by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Notifications#index&lt;/code&gt;. When Turbo eager-loads content for a Turbo Frame, it expects the url passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; to return a response that includes a Turbo Frame with a matching id.&lt;/p&gt;

&lt;p&gt;The sequence of events for our eager-loaded notifications index page is this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A logged in user visits &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dashboard#show&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dashboard#show&lt;/code&gt; is loaded&lt;/li&gt;
  &lt;li&gt;Turbo sees the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute and initiates a new request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/notifications&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Notifications#index&lt;/code&gt; returns HTML that includes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; that matches the tag that initiated the request&lt;/li&gt;
  &lt;li&gt;Turbo extracts the contents of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; and uses that content to replace the content of the existing Turbo Frame&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, logged in users can see their notifications on the dashboard. Test it out by logging in as a user and then creating a new messages for that user. Refresh the dashboard as that user and see that your notifications are listed on the dashboard:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-03-21-notification-list.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of a web page open to a list of notifications under a Notifications heading.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;While our users can see notifications, they don’t see those notifications in real-time — they have to manually refresh the dashboard before they see new notifications. Let’s wrap up this tutorial by making notifications real-time with Turbo Stream broadcasts.&lt;/p&gt;

&lt;h2 id=&quot;real-time-notifications-with-turbo-streams&quot;&gt;Real-time notifications with Turbo Streams&lt;/h2&gt;
&lt;p&gt;Turbo model broadcasts, powered by &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;turbo-rails&lt;/a&gt;, make it easy to send real-time updates to users with ActionCable. When we finish this section, each time a new notification is created, a Turbo Stream broadcast will be sent over an ActionCable channel that will automatically insert new notifications for a user into the user’s dashboard notifications list.&lt;/p&gt;

&lt;p&gt;To start, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/notification.rb&lt;/code&gt; to trigger a Turbo Stream broadcast when a new notification is created:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Noticed&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;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;polymorphic: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;

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

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;broadcast_to_recipient&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;broadcast_append_later_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;recipient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;:notifications&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;s1&quot;&gt;&apos;notifications-list&apos;&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;s1&quot;&gt;&apos;notifications/notification&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;notification: &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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_to_recipient&lt;/code&gt; method &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appends&lt;/code&gt; a new notification to the list of notifications. To ensure that only the user the notification is for receives the broadcast, we set the broadcast channel to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;recipient, :notifications&lt;/code&gt;, as described in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb#L22&quot;&gt;source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The model update handles sending Turbo Stream broadcasts, but before the broadcast will be picked up by the front end we need to subscribe users to a Turbo Stream channel. We also must ensure the markup includes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notifications-list&lt;/code&gt; id (matching the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt; passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_append_later_to&lt;/code&gt;) for the Turbo Stream to update.&lt;/p&gt;

&lt;p&gt;Starting in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/notifications/index.html.erb&lt;/code&gt;, add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notifications-list&lt;/code&gt; id to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ul&lt;/code&gt; containing the list of notifications:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;notifications&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;nt&quot;&gt;&amp;lt;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Notifications&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notifications-list&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@notifications&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/dashboard/show.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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&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;n&quot;&gt;user_signed_in?&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;
      Signed in as &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_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;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;Sign out&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;destroy_user_session_path&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text-blue-500&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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;All messages&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;messages_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;text-blue-500&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;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream_from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:notifications&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_frame_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;notifications&quot;&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;notifications_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;k&quot;&gt;else&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;s2&quot;&gt;&quot;Sign in&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_user_session_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;text-blue-500&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream_from&lt;/code&gt; helper from turbo-rails subscribes the user to a channel that matches the channel our model is broadcasting from. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user, :notifications&lt;/code&gt; in the view == &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;recipient, :notifications&lt;/code&gt; in the model).&lt;/p&gt;

&lt;p&gt;When a user visits the dashboard, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream_from&lt;/code&gt; helper opens an ActionCable subscription to the matching channel. Broadcasts to that channel are picked up by Turbo and used to update the page when new broadcasts are received. The scoping of the channel to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt; ensures that users do not receive broadcasts intended for another user so that our new message notifications are only sent to the user the message is for.&lt;/p&gt;

&lt;p&gt;With these changes in place, login as a user in one browser and head to the &lt;a href=&quot;http://localhost:3000&quot;&gt;dashboard&lt;/a&gt;. Confirm the ActionCable channel subscription is created by checking the server logs for a line that looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Turbo::StreamsChannel is streaming from Z2lkOi8vdXNlci1ub3RpY2VzL1VzZXIvMQ:notifications&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In another browser, head to the &lt;a href=&quot;http://localhost:3000/messages/new&quot;&gt;new messages form&lt;/a&gt; and create a message, setting the user on the form to the user that you are logged in as in the first browser. If all has gone well, the new notification will be added to the list instantly, with no page updates required.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-03-21-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of two web browsers open side by side. In one, a user fills out a message into a web form and submits it. In the other, the message the user wrote appears under a Notifications heading after the form is submitted.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Great work following along with this tutorial, that is all of the code for the day!&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up--further-reading&quot;&gt;Wrapping up &amp;amp; further reading&lt;/h2&gt;
&lt;p&gt;Today we built a simple notification system with Rails and Noticed, and we used Turbo to display new notifications for users in real-time. Noticed is an extremely powerful gem that makes the work of building and expanding a multi-channel notification system in a Rails app much simpler, and the easy integration with Turbo Streams makes it a great match for any modern Rails application.&lt;/p&gt;

&lt;p&gt;In our tutorial application, we displayed notifications as a static list of every notification the user has ever received, with no way to interact with them or remove them from the list. We also required users to be on their dashboard to see the new notification.&lt;/p&gt;

&lt;p&gt;In a production application, we might expand our Notifications controller to include an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; method that allows users to mark notifications as read and clear those notifications from the list.&lt;/p&gt;

&lt;p&gt;We would would also likely build a notification indicator in the main navigation that is rendered on every page of our application with an icon indicating unread notifications (think the ubiquitous bell icon seen across thousands of web applications).&lt;/p&gt;

&lt;p&gt;The neat thing about this tutorial’s approach is that the basic approach remains the same even for a more sophisticated implementation. Use Turbo Streams to broadcast new notifications to users and update the UI in real-time. Use built-in Noticed methods to act on notifications (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mark_as_read!&lt;/code&gt; to read a notification). Use a Turbo Frame to fetch notifications for a user and load those notifications into a section of a larger page.&lt;/p&gt;

&lt;p&gt;For further learning on Noticed, Turbo Streams, and Turbo Frames:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Check out the &lt;a href=&quot;https://gorails.com/episodes/rails-notifications-with-noticed&quot;&gt;GoRails tutorial&lt;/a&gt; on Noticed (Chris from GoRails wrote Noticed, thanks Chris!)&lt;/li&gt;
  &lt;li&gt;Read the simple but effective Noticed docs, starting in the &lt;a href=&quot;https://github.com/excid3/noticed&quot;&gt;repo’s readme&lt;/a&gt; and digging in to specific &lt;a href=&quot;https://github.com/excid3/noticed/tree/master/docs/delivery_methods&quot;&gt;delivery methods&lt;/a&gt; as needed&lt;/li&gt;
  &lt;li&gt;Solidify Turbo concepts with my &lt;a href=&quot;https://www.colby.so/posts/turbo-rails-101-todo-list&quot;&gt;Turbo 101 article&lt;/a&gt;, and my &lt;a href=&quot;https://www.colby.so/posts/turbo-frames-on-rails&quot;&gt;Turbo Frames&lt;/a&gt; and &lt;a href=&quot;https://www.colby.so/posts/turbo-streams-on-rails&quot;&gt;Turbo Streams&lt;/a&gt; on Rails articles, for more on how to use Turbo with Rails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, if you want to dig deeper into implementing notifications in a Rails application, a chapter of &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats&quot;&gt;my book&lt;/a&gt; is dedicated to building a very light notification system from scratch, with inspiration from Noticed. In the book, we add real-time updates in a more realistic way, with the standard bell icon, pop-out notification list, and the ability to mark notifications as read with a click. The book is written in this same, step-by-step tutorial style, and covers building a modern Rails application from scratch with StimulusReflex, CableReady, Hotwire, and friends.&lt;/p&gt;

&lt;p&gt;Building your own notification system is a great exercise if you want to use Noticed in a real production application later, since you will have a much greater understanding of, and appreciation for, what Noticed offers you.&lt;/p&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Toggling view layouts with Kredis, Turbo Frames, and Rails</title>
   <link href="https://colby.so/posts/toggling-view-layouts-with-kredis-and-rails"/>
   <updated>2022-03-07T00:00:00+00:00</updated>
   <id>https://colby.so/posts/toggling-view-layouts-with-kredis-and-rails</id>
   <content type="html">&lt;p&gt;Kredis is a new &lt;a href=&quot;https://github.com/rails/kredis&quot;&gt;gem&lt;/a&gt; that makes it easier to work with Redis keys in Ruby on Rails. Kredis &lt;a href=&quot;https://github.com/rails/rails/commit/5d9ee652013e527129792016c103ee1133106276&quot;&gt;was added&lt;/a&gt; a suggested gem for new Rails applications starting with the release of Rails 7.0 in December of 2021 and is likely to become a larger force in the Rails world in the coming years.&lt;/p&gt;

&lt;p&gt;From &lt;a href=&quot;https://github.com/rails/kredis#kredis&quot;&gt;the documentation&lt;/a&gt;, Kredis “encapsulates higher-level types and data structures around a single key, so you can interact with them as coherent objects rather than isolated procedural commands”.&lt;/p&gt;

&lt;p&gt;What this means for us is that we can use Kredis to make it easier to use Redis as a data store in our application. With Kredis, it is simple to use Redis to read and write complex data structures. Kredis’ integration with ActiveRecord allows us to work with Redis data alongside existing models.&lt;/p&gt;

&lt;p&gt;Today we will use Kredis to power a card/list view toggle for a resource’s index page, persisting the user’s view preference across requests. This tutorial will offer a gentle introduction into how Kredis works while exploring how Kredis can help us implement a common real-world UX pattern.&lt;/p&gt;

&lt;p&gt;We will also add a bit of Turbo functionality in the form of a Turbo Frame to wrap the list of items to make applying the user’s view preference a bit more efficient.&lt;/p&gt;

&lt;p&gt;When we are finished, our application will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-03-07-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application toggling a list of players between a card and a list-based layout and navigating into and back out of a player detail page.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Before we begin, this tutorial is best suited for folks with experience building simple applications with Ruby on Rails. If you have never used Rails before, this tutorial will be difficult to follow. You do not need any prior experience with Turbo or Kredis to follow along with this tutorial.&lt;/p&gt;

&lt;p&gt;As usual, you can find the complete code for this application on &lt;a href=&quot;https://github.com/DavidColby/kredis_view_toggle/tree/final&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start building.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application setup&lt;/h2&gt;
&lt;p&gt;To begin, create a new Rails 7 application from your terminal and scaffold up a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; resource, which we will use as the base for our Kredis-powered view toggle.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new kredis-players &lt;span class=&quot;nt&quot;&gt;--css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;tailwind
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;kredis-players
./bin/rails g scaffold Player name:string team:string
./bin/rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the inclusion of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;css=tailwind&lt;/code&gt; option in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails new&lt;/code&gt; command. We will use Tailwind to style our application so we can stay focused on building with Kredis instead of copy/pasting CSS.&lt;/p&gt;

&lt;p&gt;Once the application is created and the Players resource is scaffolded, you can start up the server and build Tailwind’s CSS with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because we are building a view toggle for the Players index page, you may also want to seed the database with a few players to save you some manual typing. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db/seeds.rb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Player&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;Player &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&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;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Dallas Mavericks&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then seed the database from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;./bin/rails db:seed&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;build-the-list-and-card-views&quot;&gt;Build the list and card views&lt;/h2&gt;
&lt;p&gt;Before introducing Kredis to the application, we will begin by building a list/card view toggle that allows users to swap between the two views by reading URL parameters directly in the view.&lt;/p&gt;

&lt;p&gt;Since we used the Rails scaffold generator, we already have a simple list view for Players ready on the index page. Let’s start by updating the generated views to be a little easier to process.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/_player.html.erb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;border border-gray-200 rounded&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;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;block hover:bg-gray-50&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 items-center px-4 py-4 sm:px-6&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;min-w-0 flex-1 flex 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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;min-w-0 flex-1 px-4 md:grid md:grid-cols-2 md:gap-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&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&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;player&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;/p&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;hidden md:block&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;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-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;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;team&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&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;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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;View&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player&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;text-blue-700 hover:text-blue-500&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;/p&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;/li&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Just regular ERB and some Tailwind classes for styling. Now update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-full&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Players&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;players&quot;&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;div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;space-y-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Again, just ERB and Tailwind, nothing fancy here.&lt;/p&gt;

&lt;p&gt;Next we will add the card view, starting with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_card&lt;/code&gt; partial. Create the new partial from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/_card.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then fill in the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_card&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;col-span-1 flex shadow-sm 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;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 items-center justify-between border-t border-r border-b border-gray-200 bg-white rounded-r-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;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 px-4 py-2 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;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-gray-900 font-medium hover:text-gray-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;n&quot;&gt;player&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;/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-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;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;team&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&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;px-4 py-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;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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;View&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player&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;text-blue-700 hover:text-blue-500&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;/p&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;/li&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With the card partial created, we can now add a simple toggle to the Players index page to switch between the list view and the card view.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-full&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Players&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;players&quot;&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 mt-8&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;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:view&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;s2&quot;&gt;&quot;card&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;List view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;list&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;Card view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;card&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;space-y-4&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;collection: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@players&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;players/card&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :player&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we add a messy but functional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; statement that checks the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params[:view]&lt;/code&gt;. When the view param equals card, we render the players as a grid of cards, otherwise the players are rendered as a list. Both of the view also have a link to toggle the index page to the opposite view, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to players_path(view: &quot;list&quot;/&quot;card&quot;)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This “works” as a starting point. Head to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt; and click the button to toggle between the views and the page layout changes. However, we can quickly see the limits of this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; based approach:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Toggle the index page to from a list to cards&lt;/li&gt;
  &lt;li&gt;Click on the view link for any player&lt;/li&gt;
  &lt;li&gt;Click the “Back to players” link on the show page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-03-07-unsticky-view.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application toggling from a list to a card view on the page. Then they navigate into and back out of a player detail page. When they leave the player detail page, the view has reset from a card view to a list view, losing the user&apos;s preference.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Instead of the card layout, the Players index page switches back to the list view. What is happening here? The Players index view relies on the presence of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view&lt;/code&gt; URL parameter to determine which layout to display. Because params do not persist between requests, the layout of the Players index page will always fall back to the default list view when visiting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players&lt;/code&gt; without any URL parameters.&lt;/p&gt;

&lt;p&gt;This is where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kredis&lt;/code&gt; comes in. Instead of using the url params to set the view of the Players index page, we can use Kredis to persist the user’s view preference between requests so that the index page retains the expected layout.&lt;/p&gt;

&lt;p&gt;Let’s see how Kredis works.&lt;/p&gt;

&lt;h2 id=&quot;using-kredis-to-store-preferences&quot;&gt;Using Kredis to store preferences&lt;/h2&gt;
&lt;p&gt;Kredis is a suggested gem in Rails 7, which means it is included in the default Gemfile but it is commented out. To install Kredis, first uncomment it in the Gemfile:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;- &lt;span class=&quot;c&quot;&gt;# gem &quot;kredis&quot;&lt;/span&gt;
+ gem &lt;span class=&quot;s2&quot;&gt;&quot;kredis&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt;
./bin/rails kredis:install&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Restart your Rails application after installing the Kredis gem and running the install task to avoid errors about Kredis being undefined.&lt;/p&gt;

&lt;p&gt;At this point, you will also need a Redis server running in your development environment. If you do not already have Redis installed and running, Mac users will find &lt;a href=&quot;https://gist.github.com/tomysmile/1b8a321e7c58499ef9f9441b2faa0aa8&quot;&gt;this gist&lt;/a&gt; helpful. Linux users may find &lt;a href=&quot;https://www.ubuntupit.com/how-to-install-and-configure-redis-on-linux-system/&quot;&gt;this guide&lt;/a&gt; helpful.&lt;/p&gt;

&lt;p&gt;With Kredis installed and Redis running in our local environment, we can now use Kredis to store the user’s view preference instead of relying on the presence of URL parameters.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;vi&quot;&gt;@user_preferences&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Kredis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;preferences&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@user_preferences&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;view: &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;:view&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:view&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we are using Kredis to fetch a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preferences&lt;/code&gt; key from Redis, initializing it as a &lt;a href=&quot;https://github.com/rails/kredis/blob/20aa4f272763151d462e9c9e125fcebf2eed4a5d/lib/kredis/types/hash.rb#L3&quot;&gt;hash&lt;/a&gt; and setting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_preferences&lt;/code&gt; instance variable to be a new instance of a Kredis Hash.&lt;/p&gt;

&lt;p&gt;Then, when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params[:view]&lt;/code&gt; is present in the request, we update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preferences&lt;/code&gt; hash to store the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; here updates the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preferences&lt;/code&gt; key in Redis along with updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_preferences&lt;/code&gt; instance variable.&lt;/p&gt;

&lt;p&gt;Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@user_preferences&lt;/code&gt; is an instance of a Kredis Hash, we can treat it (mostly) like a regular hash, as demonstrated by a little bit of experimenting in the console:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;irb&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:012:0&amp;gt; preferences &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; Kredis.hash&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;preferences&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;#&amp;lt;Kredis::Types::Hash:0x00007faed43f0f90&lt;/span&gt;
...
irb&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:013:0&amp;gt; preferences.keys
  Kredis Proxy &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.3ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  HKEYS preferences
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt;
irb&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:014:0&amp;gt; preferences.update&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;view: &lt;span class=&quot;s1&quot;&gt;&apos;card&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  Kredis Proxy &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.2ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  HSET preferences &lt;span class=&quot;o&quot;&gt;[{&lt;/span&gt;:view&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;card&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}]&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; 1
irb&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:015:0&amp;gt; preferences.keys
  Kredis Proxy &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.3ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  HKEYS preferences
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;view&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
irb&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:016:0&amp;gt; preferences[&lt;span class=&quot;s1&quot;&gt;&apos;view&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
  Kredis Proxy &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.3ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  HGET preferences &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;view&quot;&lt;/span&gt;&lt;span class=&quot;o&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;card&quot;&lt;/span&gt;
irb&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:017:0&amp;gt; preferences.delete&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;view&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  Kredis Proxy &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.3ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  HDEL preferences &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;view&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; 1
irb&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;main&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:018:0&amp;gt; preferences.keys
  Kredis Proxy &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.2ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  HKEYS preferences
&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To use our new persistent Kredis value instead of URL parameters when rendering the index page, head back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; and replace the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; block with the below:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;@user_preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:view&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;s2&quot;&gt;&quot;card&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;List view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;list&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;players/card&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :player&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;Card view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;card&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;space-y-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we updated the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; condition to check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@user_preferences&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; to determine which layout to render.&lt;/p&gt;

&lt;p&gt;With that change in place, head back to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt; and toggle between the list and card views. If all has gone well, toggling should still work. Even better, if you click on a player and then click to return back to the players index page, your list view preference will be retained.&lt;/p&gt;

&lt;p&gt;Neat!&lt;/p&gt;

&lt;h2 id=&quot;adding-a-turbo-frame&quot;&gt;Adding a Turbo Frame&lt;/h2&gt;
&lt;p&gt;Now that we do not need the URL parameter on every request, it also makes sense to think about what actually needs to be updated when the user toggles between the list and card views. The rest of the page will not change — only the list of players will.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames&quot;&gt;Turbo Frames&lt;/a&gt; give us the tools to adjust the application to only update the players section of the page when the user toggles the view, making the application feel faster and more responsive to user input.&lt;/p&gt;

&lt;p&gt;For our application, Turbo Frames also resolve a subtle problem that you might have encountered while testing Kredis.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive&quot;&gt;Turbo Drive&lt;/a&gt; takes a snapshot of every page to speed up applications. In almost all circumstances, this caching is something we just get for free without needing to think about; however, in this case, caching our players index page creates some issues that we need to resolve.&lt;/p&gt;

&lt;p&gt;The issue works like this:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Head to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Toggle the view from list to card&lt;/li&gt;
  &lt;li&gt;Click on the view link for a player&lt;/li&gt;
  &lt;li&gt;Click on the link to go back to the players index page&lt;/li&gt;
  &lt;li&gt;Notice that for a brief moment, the index page renders the list view before replacing it with the card view&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-03-07-content-flash.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application toggling a list of players between a card and a list-based layout and navigating into and back out of a player detail page. When they navigate back to the list page, the conent flashes from a list layout to a card-based layout.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This flash of content happens because Turbo Drive caches the index page before navigating from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players?view=card&lt;/code&gt;. The next time you visit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players&lt;/code&gt;, Drive uses the original, list-view version of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players&lt;/code&gt; from the cache to “preview” the index page before updating it with the new, card-view version of the index page rendered from the server.&lt;/p&gt;

&lt;p&gt;Turbo Frames resolve this issue because Turbo does not cache a page when navigation is scoped within a Turbo Frame. Turbo Drive’s snapshot caching only occurs when a full-page navigation occurs.&lt;/p&gt;

&lt;p&gt;This means that with Turbo Frames users are free to toggle between list and card views on the players index page as often as they like — the content of the index page will not be cached until they navigate away from the index page entirely.&lt;/p&gt;

&lt;p&gt;Because the content is not cached until the final navigation, the correct layout of the page gets cached and the “preview” displayed by Turbo on the next visit to the index page matches the final version, eliminating the flash of bad cached content.&lt;/p&gt;

&lt;p&gt;Let’s see how this works in practice.&lt;/p&gt;

&lt;p&gt;First, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; to wrap the list of players in a Turbo Frame:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-full&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Players&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;s2&quot;&gt;&quot;players&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;min-w-full mt-8&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;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@user_preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:view&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;s2&quot;&gt;&quot;card&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;List view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;list&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;players/card&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :player&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;Card view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;card&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;space-y-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we removed the “players” div and replaced that div with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame id=&quot;players&quot;&lt;/code&gt;, using the turbo-rails provided &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; helper method.&lt;/p&gt;

&lt;p&gt;Now that the list of players is wrapped in a Turbo Frame, links inside of that frame will update the content of that frame instead of updating the entire page.&lt;/p&gt;

&lt;p&gt;This means that when the user clicks on the view toggle links, Turbo will extract the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; Turbo Frame from the response and use that content to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; frame while discarding the rest of the content. When a Turbo Frame request is initiated, Rails helpfully renders the response without a layout, saving a (very small amount of) work on the server.&lt;/p&gt;

&lt;p&gt;Finish up adding Turbo Frame support by updating the links to view players in both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;card&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player&lt;/code&gt; partials like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player&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;text-blue-700 hover:text-blue-500&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;turbo_frame: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;_top&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The addition of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-frame=&quot;_top&quot;&lt;/code&gt; to links wrapped inside of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; tells Turbo to perform a normal full-page navigation, instead of scoping the navigation within the frame. This ensures that going to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players#show&lt;/code&gt; still works as expected.&lt;/p&gt;

&lt;p&gt;After making these changes, head back to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt;, toggle the view from list to card (or card to list) and then click to view a player, and finally click back out to the players index page. If all has gone well you will not see any flashing cached content.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-03-07-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application toggling a list of players between a card and a list-based layout and navigating into and back out of a player detail page.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;improving-our-kredis-usage&quot;&gt;Improving our Kredis usage&lt;/h2&gt;
&lt;p&gt;Our current implementation of Kredis will only work if our application has exactly one user. This is because we are using a static key, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preferences&lt;/code&gt;, to set the players list layout.&lt;/p&gt;

&lt;p&gt;In the real world, we will (hopefully!) have more than one user in our application. Each of our users should be able to store their own view preferences or our view toggle will not be a very useful feature! Let’s wrap up this tutorial by looking at how we can use Kredis in ActiveRecord models to store user-specific preferences, making view toggling much more useful.&lt;/p&gt;

&lt;p&gt;From your terminal, create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; model:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;./bin/rails g model User session:string
./bin/rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;helper_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:current_user&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;current_user&lt;/span&gt;    
    &lt;span class=&quot;vi&quot;&gt;@current_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_or_create_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;session: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We do not want to build out a whole user authentication system to learn more about Kredis, so here we are faking it by using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session.id&lt;/code&gt; to find or create a new user in the database and then defining &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt; as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;helper_method&lt;/code&gt; available throughout our application.&lt;/p&gt;

&lt;p&gt;In a real application, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt; would be a real, actual, logged in user, but our fake users will be good enough to demonstrate the key concept here.&lt;/p&gt;

&lt;p&gt;Our goal is to be able to associate user preferences with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; using Kredis. Our first implementation of Kredis hardcoded a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preferences&lt;/code&gt; key to store all preferences. Our new implementation will give each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; their own unique key through Kredis’ integration with ActiveRecord.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/user.rb&lt;/code&gt; and update it like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&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;kredis_hash&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:preferences&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This update means that all users will have a unique key/value pair that stores their &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preferences&lt;/code&gt; in Redis. We can access this attribute with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user.preferences&lt;/code&gt;, which will return an instance of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kredis::Hash&lt;/code&gt;, just like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kredis.hash(&apos;preferences&apos;)&lt;/code&gt; did in our controller.&lt;/p&gt;

&lt;p&gt;To use this new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preferences&lt;/code&gt; attribute, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController#index&lt;/code&gt; action in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;n&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;preferences&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;view: &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;:view&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:view&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we use the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; method we used before, this time using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user.preferences&lt;/code&gt; to access a unique hash for the current user.&lt;/p&gt;

&lt;p&gt;Then update the players index view to reference the right hash:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-full&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Players&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;s2&quot;&gt;&quot;players&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;min-w-full mt-8&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;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;preferences&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:view&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;s2&quot;&gt;&quot;card&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;List view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;list&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;grid grid-cols-1 gap-5 sm:gap-6 sm:grid-cols-2 lg:grid-cols-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;players/card&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :player&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-4&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;s2&quot;&gt;&quot;Card view&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;view: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;card&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;bg-blue-800 px-4 py-2 text-white hover:bg-blue-600 rounded&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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;role=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;list&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;space-y-4&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; statement now checks &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user.preferences&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@user_preferences&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To test out these changes, open two separate browsers, toggle the list/card view to different options in each window, and then refresh each window to see that each “user” has their own unique preferences.&lt;/p&gt;

&lt;p&gt;With that last change, you have reached the end of this tutorial! Incredible work today.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we learned a little about how to use Kredis to unlock more power from our Redis-enabled Ruby on Rails applications.&lt;/p&gt;

&lt;p&gt;Kredis’ tight integration with ActiveRecord gives it a leg up on other tools that are often used to do this type of work — if you have used Rails for a while, you have probably stuffed view preferences, filter and search terms, and other information into the session to persist the information across page turns. Redis is a more resilient and more appropriate tool for storing this type of information, and Kredis makes it simple to use Redis for this type of work in Ruby on Rails applications.&lt;/p&gt;

&lt;p&gt;While we used a hash for storing data, Kredis supports a variety of datatypes to suit your needs — explore &lt;a href=&quot;https://github.com/rails/kredis&quot;&gt;the repo&lt;/a&gt; to see the full list of available types.&lt;/p&gt;

&lt;p&gt;One of the more exciting aspects of Kredis is the tools that can be built on top of it — we can build our own simple functionality like the preference storage we created today, but other smart folks are building gems on top of Kredis to enable functionality like &lt;a href=&quot;https://github.com/julianrubisch/cubism&quot;&gt;presence tracking&lt;/a&gt; for application resources or &lt;a href=&quot;https://allfutures.leastbad.com/&quot;&gt;complex, multi-stage forms&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Hotwiring Rails Newsletter - March 2022</title>
   <link href="https://colby.so/posts/hotwiring-rails-march-2022"/>
   <updated>2022-03-02T00:00:00+00:00</updated>
   <id>https://colby.so/posts/hotwiring-rails-march-2022</id>
   <content type="html">&lt;p&gt;This newsletter went out via email to the Hotwiring Rails subscriber list, but I know that not everyone wants another email cluttering up their inbox. For those folks, I’ll always publish the full content of the newletter here too, so you can get all the content with none of the emails.&lt;/p&gt;

&lt;p&gt;Thoughts or feedback on how I can make this newsletter more valuable? Have something you’d like to include in next month’s edition? Send me an &lt;a href=&quot;mailto:david@colby.so?subject=Hotwiring+Rails&quot;&gt;email&lt;/a&gt;, or find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#articles-and-guides&quot;&gt;Articles and guides&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#my-recent-writing&quot;&gt;My recent writing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;articles-and-guides&quot;&gt;Articles and guides&lt;/h2&gt;

&lt;h3 id=&quot;stripe-checkout-and-customer-portal-ruby-on-rails-tutorial-by-andy-leverenz&quot;&gt;Stripe checkout and customer portal Ruby on Rails Tutorial by Andy Leverenz&lt;/h3&gt;

&lt;p&gt;Building payment infrastructure is always challenging. This in-depth tutorial and accompanying screencast, if video is your thing, is an excellent resource for quickly integrating Stripe into your application.&lt;/p&gt;

&lt;p&gt;I love long, detailed tutorials, and this one checks all the boxes. If you need to get Stripe checkout setup and aren’t sure where to start, this is the place.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://web-crunch.com/posts/stripe-checkout-billing-portal-ruby-on-rails&quot;&gt;Read the tutorial on web-crunch&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-in-depth-guide-to-activerecord-load_async-in-rails-7-by-paweł-urbanek&quot;&gt;The In-depth Guide to ActiveRecord load_async in Rails 7 by Paweł Urbanek&lt;/h3&gt;

&lt;p&gt;Paweł’s article on the load_async ActiveRecord feature &lt;a href=&quot;https://github.com/rails/rails/pull/41372&quot;&gt;added in Rails 7&lt;/a&gt; is an exceptionally detailed look at how load_async works, its potential dangers, and the right way to approach using load_async in your Rails applications.&lt;/p&gt;

&lt;p&gt;Highly recommended for anyone working on production Rails apps to keep one around as a resource.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://pawelurbanek.com/rails-load-async&quot;&gt;Read the guide on pawelurbanek.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;exploring-trix-by-konnor-rogers&quot;&gt;Exploring Trix by Konnor Rogers&lt;/h3&gt;

&lt;p&gt;This in-progress series explores techniques for extending and enhancing the functionality of Trix. Trix’s integration with Rails is really convenient, but the limitations of the editor have made it hard to adopt for many use cases. Konnor’s work in this series makes Trix more valuable.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/paramagicdev/exploring-trix-34hb&quot;&gt;Read the series on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;hotrailsdev-by-alexandre-rubin&quot;&gt;Hotrails.dev by Alexandre Rubin&lt;/h3&gt;

&lt;p&gt;This multi-part tutorial on building an application with Turbo and one line of custom JavaScript is a wonderful introduction to Turbo. I especially appreciate the annotated illustrations of common Turbo concepts.&lt;/p&gt;

&lt;p&gt;This is very much worth the time for folks new to turbo-rails.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.hotrails.dev/&quot;&gt;Start the course&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;toggling-page-content-with-turbo-frames-and-kredis-by-peter-phillips&quot;&gt;Toggling page content with Turbo Frames and Kredis by Peter Phillips&lt;/h3&gt;

&lt;p&gt;I love Kredis, and I am excited for more folks to start creating cool stuff with it. In this quick tutorial, you’ll learn how to combine Kredis and Turbo Frames to toggle the visibility of page content, and to persist that preference across page loads.&lt;/p&gt;

&lt;p&gt;This is a great introduction to Kredis, and the types of problems that Kredis can help you solve.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/phillipspc/toggling-page-content-with-turbo-frames-and-kredis-20no&quot;&gt;Read it on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;global-presence-by-overstimulated&quot;&gt;Global presence by overstimulated&lt;/h3&gt;

&lt;p&gt;This is a really neat demonstration of a multi-region application built on fly.io, exploring this extremely complex question: “what does it look like to hook up a system of live-DOM-updates-over-Websockets to a caching layer that’s distributed across multiple regions?”&lt;/p&gt;

&lt;p&gt;The end result is a really neat example of some of the problem spaces that are now accessible to determined Rails developers.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/overstimulat3d/global-presence-i-made-a-thing-98o&quot;&gt;Read&lt;/a&gt; the overview, &lt;a href=&quot;https://global-presence.fly.dev/&quot;&gt;try out&lt;/a&gt; the demo, or &lt;a href=&quot;https://github.com/Matt-Yorkley/global-presence&quot;&gt;view the source&lt;/a&gt; on Github.&lt;/p&gt;

&lt;h3 id=&quot;building-a-lightbox-with-hotwire-and-swiperjs-by-julian-rubisch&quot;&gt;Building a Lightbox with Hotwire and Swiper.js by Julian Rubisch&lt;/h3&gt;

&lt;p&gt;Julian’s take on adding a simple lightbox to a Rails application is a great resource, approachable for all levels of Rails developers and the step-by-step approach makes it accessible even if you are new to Turbo and Stimulus.&lt;/p&gt;

&lt;p&gt;This is a great one to bookmark for the next time you need to add a lightbox to a Rails application you maintain :)&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://blog.minthesize.com/lightbox-hotwire-swiper&quot;&gt;Read the article on minthesize.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;modern-front-end-magic-with-rails-7-a-visual-editor-for-markdown-by-kuyio&quot;&gt;Modern Front End Magic With Rails 7: A Visual Editor for Markdown by KUY.io&lt;/h3&gt;

&lt;p&gt;This series released in October 2021 but I first came across it this month and found it to be an exceptionally thoughtful and detailed look at solving a hard problem with Rails and Hotwire.&lt;/p&gt;

&lt;p&gt;The two part series walks through building a WYSIWYG markdown editor from scratch in a Rails application with plenty of Turbo and Stimulus in the mix.&lt;/p&gt;

&lt;p&gt;Read &lt;a href=&quot;https://kuy.io/blog/posts/modern-front-end-magic-with-rails-7-a-visual-editor-for-markdown-part-1&quot;&gt;part one&lt;/a&gt; and &lt;a href=&quot;https://kuy.io/blog/posts/modern-front-end-magic-with-rails-7-a-visual-editor-for-markdown-part-2&quot;&gt;part two&lt;/a&gt; of the series on KUY.io&lt;/p&gt;

&lt;h3 id=&quot;load-more-pagination-in-rails-with-hotwire-turbo-streams-by-dale-zak&quot;&gt;Load More Pagination in Rails with Hotwire Turbo Streams by Dale Zak&lt;/h3&gt;

&lt;p&gt;Everyone needs to paginate resources — Dale Zak has a great, quick write up on building pagination from scratch with Turbo and Stimulus.&lt;/p&gt;

&lt;p&gt;This article includes a really neat way to send Turbo Stream GET requests with a small Stimulus controller to modify headers — a handy trick to have in your Turbo bag.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/dalezak/load-more-pagination-in-rails-with-hotwire-turbo-streams-433e&quot;&gt;Read the write up on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/h2&gt;

&lt;h3 id=&quot;devise-alternatives&quot;&gt;Devise alternatives&lt;/h3&gt;

&lt;p&gt;Last month, I linked to Steve Polito’s &lt;a href=&quot;https://stevepolito.design/blog/rails-authentication-from-scratch/&quot;&gt;write up&lt;/a&gt; on building a Rails authentication system from scratch. A few weeks ago, Steve released an &lt;a href=&quot;https://github.com/stevepolitodesign/rails_mvp_authentication&quot;&gt;authentication generator&lt;/a&gt; for Rails 7 applications that allows anyone to add a simple authentication system to their Rails application.&lt;/p&gt;

&lt;p&gt;The generator-based approach makes it easier to wrap your head around what your authentication system is doing, and to extend and modify it as needed.&lt;/p&gt;

&lt;p&gt;Lázaro Nixon &lt;a href=&quot;https://github.com/lazaronixon/authentication-zero&quot;&gt;released a similar generator-based approach&lt;/a&gt; this month too.&lt;/p&gt;

&lt;p&gt;I would still choose Devise for most commercial applications today, but the movement towards a more Rails-native approach to scalable, secure authentication is exciting and continues to receive support from the Rails core team. The challenges of integrating Devise into Turbo-enabled applications are likely to drive even more interest here in the coming months.&lt;/p&gt;

&lt;h3 id=&quot;an-extremely-comprehensive-hotwire-rails-template&quot;&gt;An extremely comprehensive Hotwire Rails template&lt;/h3&gt;

&lt;p&gt;Georg Ledermann’s opinionated &lt;a href=&quot;https://github.com/ledermann/templatus-hotwire&quot;&gt;Hotwire Rails template&lt;/a&gt; is a great starting place for a new Rails 7 application. The template goes well beyond the standard configuration options you’ll find in most templates — you’ll find linting, testing, and deployment needs are fully covered here too.&lt;/p&gt;

&lt;h2 id=&quot;my-recent-writing&quot;&gt;My recent writing&lt;/h2&gt;

&lt;h3 id=&quot;i-wrote-a-book&quot;&gt;I wrote a book!&lt;/h3&gt;

&lt;p&gt;I am very pleased to share that the book on modern Rails development that I have been working on since the fall is now “published”, which means a little bit of &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats/68y8c5c&quot;&gt;self-promotion&lt;/a&gt; in this edition of the newsletter, and a much more regular cadence of blogs in the coming months now that I’m not writing a book and building a web app to publish the book as something more useful than a PDF.&lt;/p&gt;

&lt;p&gt;The book, Hotwired ATS, is available &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats/68y8c5c&quot;&gt;on Gumroad at a 20% discount&lt;/a&gt; until March 3rd — if you prefer reading context-less source code, you can also find the source &lt;a href=&quot;https://github.com/DavidColby/hotwired_ats_code&quot;&gt;on Github&lt;/a&gt; for free.&lt;/p&gt;

&lt;h3 id=&quot;real-time-previews-with-rails-and-stimulusreflex&quot;&gt;Real-time previews with Rails and StimulusReflex&lt;/h3&gt;

&lt;p&gt;My exploration of markdown editors that led to the wonderful series from KUY.io linked above started with putting together this &lt;a href=&quot;https://www.colby.so/posts/real-time-previews-with-stimulus-reflex&quot;&gt;introductory-level tutorial&lt;/a&gt; on using StimulusReflex to preview markdown content as the user types.&lt;/p&gt;

&lt;h3 id=&quot;pagination-and-infinite-scrolling-with-rails-and-the-hotwire-stack&quot;&gt;Pagination and infinite scrolling with Rails and the Hotwire stack&lt;/h3&gt;

&lt;p&gt;Dale Zak’s article on pagination linked above, and a conversation on the StimulusReflex discord, inspired &lt;a href=&quot;https://www.colby.so/posts/pagination-and-infinite-scrolling-with-hotwire&quot;&gt;this article&lt;/a&gt; that introduces another approach to both traditional pagination and infinite scrolling via a small Stimulus controller.&lt;/p&gt;

&lt;h3 id=&quot;turbo-rails-101-build-a-todo-app-with-turbo&quot;&gt;Turbo Rails 101: Build a todo app with Turbo&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://www.colby.so/posts/turbo-rails-101-todo-list&quot;&gt;This one&lt;/a&gt; does what it says on the tin: It is a brand new, “what the hell is a Turbo Frame and why do I care”-level introduction to Turbo Frames and Turbo Streams aimed at alleviating a couple of common sticking points for new Turbo users.&lt;/p&gt;

&lt;h2 id=&quot;the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/h2&gt;

&lt;h3 id=&quot;spam-accounts-in-2022-by-josh-beckman&quot;&gt;Spam accounts in 2022 by Josh Beckman&lt;/h3&gt;

&lt;p&gt;This is a &lt;a href=&quot;https://www.joshbeckman.org/what-spam-accounts-look-like-in-2022/&quot;&gt;quick, interesting read&lt;/a&gt; on the tactics that spammers use to game search engines and drive affiliate sales (in the best cases) or steal identities and cash (in the worst cases).&lt;/p&gt;

&lt;p&gt;If you have ever built an SaaS that gives customers access to post jobs, build a website, or otherwise create public-facing content that can be used to run a scam, you’ve probably dealt with tactics like this.&lt;/p&gt;

&lt;h3 id=&quot;server-sent-events-websockets-and-http-by-mark-notttingham&quot;&gt;Server-Sent Events, WebSockets, and HTTP by Mark Notttingham&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://www.mnot.net/blog/2022/02/20/websockets&quot;&gt;This piece&lt;/a&gt; was near the top of the dreaded orange site recently and I found it to be a really interesting read on the challenges of making pub/sub a standards-defined part of the web.&lt;/p&gt;

&lt;p&gt;The work that goes into defining and expanding the standards that enables web developers to build incredibly complex experiences that work seamlessly across browsers and devices always leaves me feeling awed and slightly intimated. Writing like this that helps illuminate some of thinking that goes into how to solve these types of standards-defining problems is always a treat.&lt;/p&gt;

&lt;h2 id=&quot;until-next-time&quot;&gt;Until next time&lt;/h2&gt;

&lt;p&gt;If you’re a Rails developer who also happens to love filling out surveys, the folks at Planet Argon &lt;a href=&quot;https://planetargon.survey.fm/rails-survey-2022&quot;&gt;have a treat&lt;/a&gt; for you.&lt;/p&gt;

&lt;p&gt;The link to apply for a RailsConf &lt;a href=&quot;https://docs.google.com/forms/d/e/1FAIpQLSfWzLxRmCVmNVFXaPc9QrOhyws564DFiE3y5jQhDiQPxF6xWw/viewform&quot;&gt;opportunity scholarship&lt;/a&gt; is open until March 11.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Turbo Rails 101&#58; Building a todo app with Turbo</title>
   <link href="https://colby.so/posts/turbo-rails-101-todo-list"/>
   <updated>2022-02-11T00:00:00+00:00</updated>
   <id>https://colby.so/posts/turbo-rails-101-todo-list</id>
   <content type="html">&lt;p&gt;When Rails 7 released in December, Turbo became a default component of all new Ruby on Rails applications. This was an exciting moment for Rails developers that want to use Rails full-stack — especially folks on small teams with limited resources to pour into building and maintaining a React or Vue frontend.&lt;/p&gt;

&lt;p&gt;Along with that excitement has come a constant stream of developers trying to learn how Turbo works, and how to integrate it into their Rails applications successfully.&lt;/p&gt;

&lt;p&gt;For many, that learning journey has included a lot of roadblocks — Turbo requires some changes to how you structure applications and the official documentation is not yet as detailed as it could be. The good news is, most folks hit the same roadblocks early on their journey, which means we can help folks faster by addressing the common points of confusion.&lt;/p&gt;

&lt;p&gt;In particular, there is confusion about how to use Turbo Frames and Turbo Streams together, and confusion about how Turbo Streams work.&lt;/p&gt;

&lt;p&gt;Today, we are going to build a simple todo list application, powered entirely by Turbo. While building, we will take a few detours to look more deeply at a few common Turbo behaviors, and we will directly address two of the most common misconceptions that I see from folks who are brand new to using Turbo in their Rails applications.&lt;/p&gt;

&lt;p&gt;When we are finished, our application will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-11-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application adding and removing todo items from a list.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This tutorial is written for Rails developers who are brand new to Turbo. The content is very much Turbo 101 and may not be useful if you are already comfortable working with Turbo Frames and Turbo Streams. This article also assumes comfort with writing standard CRUD applications in Rails — if you are have never used Rails before, this is not the right place to start!&lt;/p&gt;

&lt;p&gt;As usual, you can find the complete code for our demo application on &lt;a href=&quot;https://github.com/DavidColby/turbo-todo&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start building!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application setup&lt;/h2&gt;
&lt;p&gt;We will start with a brand new Rails 7 application which comes with Turbo out of the box.&lt;/p&gt;

&lt;p&gt;Generate a new Rails application with Tailwind CSS for styling. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new turbo-todo &lt;span class=&quot;nt&quot;&gt;--css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;tailwind
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;turbo-todo&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then scaffold up a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; resource. From your terminal again:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g scaffold Todo name:string status:integer&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Update migration to set default value for status:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateTodos&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;create_table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:todos&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;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;integer&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;default: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, create and migrate the database:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails db:create db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;create-new-todos-with-turbo-streams&quot;&gt;Create new todos with Turbo Streams&lt;/h2&gt;
&lt;p&gt;The Rails scaffold generator provides a fully functional implementation of todos out of the box. If you start up the Rails app and head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/todos&lt;/code&gt; you can create, edit, and delete todos to your heart’s content, but every request will initiate a full page turn. Not very exciting.&lt;/p&gt;

&lt;p&gt;Our goal in this section is to update the existing todo scaffold to use Turbo Streams to insert newly created todos into the DOM without a full page turn or any custom JavaScript.&lt;/p&gt;

&lt;p&gt;Start by replacing the content of the index view, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/todos/index.html.erb&lt;/code&gt;, with the following:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mx-auto w-1/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;h2&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-2xl text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    Your todos
  &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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-full max-w-2xl bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;py-2 px-4&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&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;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;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;todos&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@todos&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we updated the page layout so it looks a little nicer and inserted the new todo form directly on to the page. Users will use this form to add new todos, and existing todos will be rendered in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; below the form.&lt;/p&gt;

&lt;p&gt;Note that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; has an id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos&lt;/code&gt;. Turbo Streams target elements in the DOM by id, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos&lt;/code&gt; id will be used to insert newly created todos into the DOM.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/todos/_todo.html.erb&lt;/code&gt; to render each todo properly inside of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;py-2 px-4 border-b border-gray-300&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;todo&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; partial we render in the index view needs a few adjustments too. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/todos/_form.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;todo&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;_form&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error_explanation&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3&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&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluralize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error&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; prohibited this todo from being saved:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&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;error&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;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex items-stretch flex-grow&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sr-only&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block w-full rounded-none rounded-l-md sm:text-sm border-gray-300&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Add a new todo...&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;-ml-px relative px-4 py-2 border border-blue-600 text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the addition of an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; passed to the partial, which will be used to target Turbo Stream updates.&lt;/p&gt;

&lt;p&gt;To use these new ids to update the DOM, we need to tell our controller to render a Turbo Stream when the form is submitted.&lt;/p&gt;

&lt;p&gt;To do this, head to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; action:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@todo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Todo&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;todo_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@todo&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;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;turbo_stream&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;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;todo_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;Todo was successfully created.&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;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;p&quot;&gt;{&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The change here is the addition of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format.turbo_stream&lt;/code&gt; to the happy path in the action. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format.turbo_stream&lt;/code&gt; tells Rails that when a Turbo Stream request is sent to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; action, respond with a matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;If you are a long-time Rails developer, this will feel very similar to responding with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;js.erb&lt;/code&gt; files in response to ajax requests.&lt;/p&gt;

&lt;p&gt;In order for this to work, we need to create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt; file, otherwise you will get an error about a missing template.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/todos/create.turbo_stream.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then fill that new file in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;prepend&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;todos&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;s2&quot;&gt;&quot;todo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;no&quot;&gt;Todo&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;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_form&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;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our first look at a Turbo Stream! In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt; we render two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;The first &lt;a href=&quot;https://turbo.hotwired.dev/reference/streams#prepend&quot;&gt;prepends&lt;/a&gt; the newly created todo to the list of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos&lt;/code&gt;, targeting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; with the id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos&lt;/code&gt;. The second &lt;a href=&quot;https://turbo.hotwired.dev/reference/streams#replace&quot;&gt;replaces&lt;/a&gt; the todo form with a fresh copy of the form, allowing us to clear the todo form after each successful submission.&lt;/p&gt;

&lt;p&gt;At this point, you can start up your Rails application with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt;. Head to &lt;a href=&quot;http://localhost:3000/todos&quot;&gt;localhost:3000/todos&lt;/a&gt;, create a couple of todos and see that they automatically append to the list of todos. Magic.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-11-plain-prepend.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application adding a todo item to the list of todos&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Let’s pause here and review what is happening in a little more detail. Each time the user submits the new todo form, the request sent to the server includes an &lt;a href=&quot;https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses&quot;&gt;Accept header&lt;/a&gt; that identifies the request as a Turbo Stream request:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;text/vnd.turbo-stream.html, text/html, application/xhtml+xml&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Turbo sets this header automatically on all &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUT&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PATCH&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt; form submissions, with no intervention required from the developer. As of &lt;a href=&quot;https://github.com/hotwired/turbo/releases/tag/v7.2.0&quot;&gt;Turbo 7.2&lt;/a&gt;, you can optionally respond Turbo Streams in response to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt; requests. Just include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; on the link or GET form you want to process as a Turbo Stream.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/2a2a5ec12e4233cf33e5d3f0a6855834f4e5e4ec/lib/turbo/engine.rb#L51&quot;&gt;turbo-rails&lt;/a&gt; registers a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; Mime type to enable responding to inbound Turbo Stream form submissions with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; content. We see this in action in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;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;turbo_stream&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format.turbo_stream&lt;/code&gt; without passing a block, Rails conventions expect that a file that matches the action and Mime type exists — in our case, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt;, we render &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; elements using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; helper. Rails renders the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream&lt;/code&gt; view to HTML and sends that HTML back to the frontend:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;prepend&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;todos&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;template&amp;gt;&lt;/span&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;py-2 px-4 border-b border-gray-300&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      A new todo
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;

&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;replace&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;new_todo_form&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;template&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;new_todo_form&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/todos&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;accept-charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;UTF-8&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;method=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;post&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;authenticity_token&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;TmpPYfxQOln1t3jmigbzZ49ciBWjivGEjp_nJUWJMeUJZSoeA8dvbkLeD6kLkmHZx-zc8kOzcqu69tWGYASlpQ&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;autocomplete=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;off&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 items-stretch flex-grow&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;label&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;na&quot;&gt;for=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;todo_name&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;block w-full rounded-none rounded-l-md sm:text-sm border-gray-300&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;placeholder=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Add a new todo...&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;todo[name]&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;todo_name&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;input&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;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;commit&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Create Todo&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;-ml-px relative px-4 py-2 border border-blue-600 text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-disable-with=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Create Todo&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;/form&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Turbo extracts the Turbo Stream elements from the HTML and uses each element’s action and content to update the DOM.&lt;/p&gt;

&lt;p&gt;Now we understand a bit about what is happening when our Turbo-powered from is submitted, which also gives us the knowledge to knock out a few common misconceptions about Turbo.&lt;/p&gt;

&lt;h3 id=&quot;turbo-streams-can-target-any-element-not-just-turbo-frames&quot;&gt;Turbo Streams can target any element, not just Turbo Frames&lt;/h3&gt;
&lt;p&gt;First, many new Turbo developers think that Turbo Streams can only target Turbo Frames. This misconception causes them to run into issues nesting their forms within an unnecessary Turbo Frame or to end up with invalid HTML by wrapping &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;li&amp;gt;&lt;/code&gt; elements in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;Issues caused by this misconception come up almost daily on the Rails Internet and Turbo Streams can be very difficult to work with while laboring under this misconception. Although the documentation never links Turbo Frames and Turbo Streams in this way, the issue persists.&lt;/p&gt;

&lt;p&gt;So, let’s get very clear here: Turbo Streams target elements in the DOM by id (or &lt;a href=&quot;https://github.com/hotwired/turbo/pull/113&quot;&gt;class&lt;/a&gt;, less commonly). Any element with an id can be targeted by a Turbo Stream, not just Turbo Frame elements.&lt;/p&gt;

&lt;h3 id=&quot;turbo-streams-do-not-require-websockets&quot;&gt;Turbo Streams do not require WebSockets&lt;/h3&gt;
&lt;p&gt;Turbo Streams have gotten a lot of attention because they can be used with WebSockets to proactively send updates to many users at once outside of the standard request/response cycle.&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt;, developers can easily &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast&lt;/code&gt; updates from models and background jobs to send &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; snippets over WebSockets with  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionCable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These types of WebSockets-powered Turbo Stream broadcasts are great — but as you saw in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt;, you can also just render Turbo Stream tags in response to a request from a browser. You can make great use of Turbo Streams without WebSockets.&lt;/p&gt;

&lt;p&gt;Now that we have gotten way down into the weeds of Turbo Streams, let’s zoom back out a bit and take our first look at Turbo Frames.&lt;/p&gt;

&lt;h2 id=&quot;editing-existing-todos&quot;&gt;Editing existing todos&lt;/h2&gt;
&lt;p&gt;Users will edit their todos by clicking on the name of the todo. When they click on the name, the edit form for that todo will render in place of the todo in the list, like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-11-editing-todos.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application clicking a todo in a list. An edit form replaces the todo item in the list.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To build this functionality, we will use a Turbo Frame to scope navigation to the piece of the page we want to update. Start by updating the existing todo partial like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;todo&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;_container&quot;&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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;py-2 px-4 border-b border-gray-300&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;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;n&quot;&gt;todo&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;n&quot;&gt;todo&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;edit_todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&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;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we added a unique id to each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;li&amp;gt;&lt;/code&gt;. Nested within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;li&amp;gt;&lt;/code&gt;, we added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; provided &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; helper method.&lt;/p&gt;

&lt;p&gt;Within the Turbo Frame is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt; pointing to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt; action in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt;. Because the link is within the Turbo Frame, Turbo will expect the server to return a Turbo Frame with a matching id. Turbo will extract the matching Turbo Frame from the response HTML and use it to replace the original content of the frame.&lt;/p&gt;

&lt;p&gt;In our case, that means when the user clicks on the todo’s name, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit.html.erb&lt;/code&gt; will render an edit form and that form will replace the link to the edit page.&lt;/p&gt;

&lt;p&gt;Let’s see this in action. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/edit.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;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;@todo&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; has an id that matches the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; partial.&lt;/p&gt;

&lt;p&gt;With that change in place, refresh the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos&lt;/code&gt; index page and click on the name of a todo. If all has gone well, you will see that the edit form replaces that todo in the list. If you submit the form, you will see that you get redirected to the show page of the todo you edited — not quite there yet!&lt;/p&gt;

&lt;p&gt;We will fix this issue with another Turbo Stream rendered from the server, this time for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController#update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/todos/update.turbo_stream.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Update the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update.turbo_stream.erb&lt;/code&gt; view like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;vi&quot;&gt;@todo&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;_container&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;s2&quot;&gt;&quot;todo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We are using a Turbo Stream &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; action again. This time the Turbo Stream action replaces the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;li&amp;gt;&lt;/code&gt; wrapping the todo with the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todo&lt;/code&gt; partial. Because the edit form is replaced by the updated Todo, we do not need to reset the edit form like we did the new form in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt; to respond to Turbo Stream requests:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@todo&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;todo_params&lt;/span&gt;&lt;span class=&quot;p&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;turbo_stream&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;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;todo_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;Todo was successfully updated.&quot;&lt;/span&gt; &lt;span class=&quot;p&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;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;:show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: :ok&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;vi&quot;&gt;@todo&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;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;p&quot;&gt;{&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;p&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;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;@todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now when the edit form is submitted successfully the edit form is replaced with an updated version of the edited todo’s partial.&lt;/p&gt;

&lt;p&gt;At this point, we can create and edit todos without a full page turn but you may have noticed that we are not using Turbo Streams to handle invalid form submissions. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;else&lt;/code&gt; path in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; actions is missing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; response. We will fix that in the next section.&lt;/p&gt;

&lt;h2 id=&quot;handling-form-errors&quot;&gt;Handling form errors&lt;/h2&gt;
&lt;p&gt;To demonstrate handling form errors, we need to first add a validation to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; model so that we can send invalid form submissions to the server. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/todo.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;validates_presence_of&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:name&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Form submissions with a blank &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; will fail to save, letting us test out error handling with Turbo Streams.&lt;/p&gt;

&lt;p&gt;Head back to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt; and update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; actions:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@todo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Todo&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;todo_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@todo&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;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;turbo_stream&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;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;todo_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;Todo was successfully created.&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;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;turbo_stream&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&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;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;_form&quot;&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;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;todo: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;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;p&quot;&gt;{&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;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;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@todo&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;todo_params&lt;/span&gt;&lt;span class=&quot;p&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;turbo_stream&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;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;todo_url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;Todo was successfully updated.&quot;&lt;/span&gt; &lt;span class=&quot;p&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;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;:show&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: :ok&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;vi&quot;&gt;@todo&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;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;turbo_stream&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&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;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;_form&quot;&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;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;todo: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;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;p&quot;&gt;{&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;p&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;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;@todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When the todo fails to save, we render a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; directly from the controller, replacing the content of the form with an updated version of the form so that errors are displayed to the user.&lt;/p&gt;

&lt;p&gt;This method of rendering Turbo Streams inline in the controller is an alternative to creating views like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt;  — either approach will work. In practice, it tends to be easier to manage complex Turbo Stream responses with dedicated views while rendering simple responses inline works fine for single stream responses.&lt;/p&gt;

&lt;h2 id=&quot;deleting-todos&quot;&gt;Deleting todos&lt;/h2&gt;
&lt;p&gt;Next up, we will add the ability to delete todos without a page turn by using another Turbo Stream rendered from the controller.&lt;/p&gt;

&lt;p&gt;Start by updating the todo partial to add a delete button:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;todo&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;_container&quot;&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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;py-2 px-4 border-b border-gray-300&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;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;n&quot;&gt;todo&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 justify-between 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;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;todo&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;edit_todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&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;button_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&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;bg-red-600 px-4 py-2 rounded hover:bg-red-700&quot;&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;
        &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;Delete&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 text-white&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;na&quot;&gt;fill=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Delete todo&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;fill-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;clip-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&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;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;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;method: :delete&lt;/code&gt; on the button, which ensures the button hits the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destroy&lt;/code&gt; action on the controller. The svg icon here is from &lt;a href=&quot;https://heroicons.com/&quot;&gt;Heroicons&lt;/a&gt;  — feel free to just make the button say “Delete” if you like, that will work fine too.&lt;/p&gt;

&lt;p&gt;In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt;, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destroy&lt;/code&gt; action:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;destroy&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;destroy&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;turbo_stream&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;remove&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;helpers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;_container&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;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;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;todos_url&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;Todo was successfully destroyed.&quot;&lt;/span&gt; &lt;span class=&quot;p&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;json&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:no_content&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This inline &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; uses the Turbo Stream &lt;a href=&quot;https://turbo.hotwired.dev/reference/streams#remove&quot;&gt;remove&lt;/a&gt; action to remove the target element from the DOM entirely.&lt;/p&gt;

&lt;p&gt;Refresh the index page, click the delete button on a todo and see that the todo is removed from the DOM without a full page turn.&lt;/p&gt;

&lt;h2 id=&quot;mark-todos-as-complete&quot;&gt;Mark todos as complete&lt;/h2&gt;
&lt;p&gt;A todo list is not very helpful if todos cannot be marked as complete. In this section, we will add a button to toggle todos complete and incomplete, relying as usual on Turbo Streams to update the DOM for us.&lt;/p&gt;

&lt;p&gt;To begin, let’s define a simple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; enum in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Todo&lt;/code&gt; model:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;incomplete: &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;complete: &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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/todos/_todo.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;todo&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;_container&quot;&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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;py-2 px-4 border-b border-gray-300&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;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;n&quot;&gt;todo&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 justify-between 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;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;todo&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;edit_todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&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;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;complete?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;line-through&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;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-end space-x-3&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;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;complete?&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;n&quot;&gt;todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &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;s1&quot;&gt;&apos;incomplete&apos;&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;bg-green-600 px-4 py-2 rounded hover:bg-green-700&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :patch&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;sr-only&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Mark as incomplete&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 text-white&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;na&quot;&gt;fill=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&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;fill-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;clip-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&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;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;else&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;n&quot;&gt;todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &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;s1&quot;&gt;&apos;complete&apos;&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;bg-gray-400 px-4 py-2 rounded hover:bg-green-700&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :patch&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;sr-only&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Mark as complete&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 text-white&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;na&quot;&gt;fill=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&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;fill-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;clip-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&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;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;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;todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&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;bg-red-600 px-4 py-2 rounded hover:bg-red-700&quot;&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;
          &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;Delete&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 text-white&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;na&quot;&gt;fill=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Delete todo&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;fill-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;clip-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&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;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&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There’s a lot here, let’s cut through the noise to highlight the important functional pieces.&lt;/p&gt;

&lt;p&gt;The todo edit link gets struck through when the todo is complete:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;todo&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;edit_todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&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;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;complete?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;line-through&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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If the todo is complete, we render &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button_to&lt;/code&gt; to mark the todo as incomplete. Incomplete todos get a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;button_to&lt;/code&gt; to mark the todo as complete.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;complete?&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;n&quot;&gt;todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &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;s1&quot;&gt;&apos;incomplete&apos;&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;bg-green-600 px-4 py-2 rounded hover:bg-green-700&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :patch&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;else&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;n&quot;&gt;todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &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;s1&quot;&gt;&apos;complete&apos;&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;bg-gray-400 px-4 py-2 rounded hover:bg-green-700&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :patch&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In either case the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patch&lt;/code&gt; request goes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController#update&lt;/code&gt; as a Turbo Stream request, and the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update.turbo_stream.erb&lt;/code&gt; view is rendered.&lt;/p&gt;

&lt;p&gt;If this were a real application, we would pull these buttons out into helper methods or into a view component, but for our purposes, we can live with a messy partial.&lt;/p&gt;

&lt;h2 id=&quot;completeincomplete-todos-in-separate-tabs&quot;&gt;Complete/incomplete todos in separate tabs&lt;/h2&gt;
&lt;p&gt;Now that users can mark todos as complete, it would be nice to not have to see completed todos all of the time. We will finish up our Turbo-powered todo application by adding a tabbed interface to the todos index page, allowing users to toggle between incomplete and complete todos.&lt;/p&gt;

&lt;p&gt;Get started by adding simple filtering logic to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@todos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Todo&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;status: &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;:status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;presence&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;incomplete&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/todos/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mx-auto w-1/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;h2&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-2xl text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    Your todos
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&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;s2&quot;&gt;&quot;todos-container&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;block max-w-2xl w-full bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-sm&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;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;border-b border-gray-200 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;ul&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-2 justify-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;li&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;Incomplete&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;todos_path&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;s2&quot;&gt;&quot;incomplete&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;inline-block py-4 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300&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;li&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;Complete&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;todos_path&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;s2&quot;&gt;&quot;complete&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;inline-block py-4 px-4 text-sm font-medium text-center text-gray-500 border-b-2 border-transparent hover:text-gray-600 hover:border-gray-300&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;/li&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;unless&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;:status&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;s2&quot;&gt;&quot;complete&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;py-2 px-4&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Todo&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;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;span class=&quot;nt&quot;&gt;&amp;lt;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;todos&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@todos&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The index view now wraps the todo content in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos-container&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt;. As with the edit links for each individual todo, this Turbo Frame will scope navigation within the frame, allowing the list of Todos to be updated without changing the content on the rest of the page.&lt;/p&gt;

&lt;p&gt;Inside of the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos-container&lt;/code&gt; frame, we added links to view incomplete and complete todos. Logic to hide the new todo form when viewing complete todos was also added, since newly created todos are always incomplete.&lt;/p&gt;

&lt;p&gt;Because the links to view incomplete and complete todos are within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos-container&lt;/code&gt; Turbo Frame, each time those links are clicked, Turbo will replace the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos-container&lt;/code&gt; with updated content from the server.&lt;/p&gt;

&lt;p&gt;Conveniently, we do not need to change anything about the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action to render Turbo Frame content. Even though the entire page re-renders when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action is called, Turbo will extract the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos-container&lt;/code&gt; frame from the response and discard the rest. If that small bit of inefficiency bothers you, it is possible to be &lt;a href=&quot;https://www.colby.so/posts/turbo-frames-on-rails#conditional-template-rendering-using-variants&quot;&gt;more efficient&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this change in place, we have a slight problem with the status toggle behavior. Right now, when the user marks a todo as complete, the todo is updated but it stays on the list. Instead, when a todo’s status is updated, we would like to remove it from the list.&lt;/p&gt;

&lt;p&gt;Implementing this functionality will require adding a new, non-RESTful action to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt;. We will call this new action &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change_status&lt;/code&gt;. Start in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodosController&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set_todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[ show edit update destroy change_status ]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;change_status&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@todo&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;status: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo_params&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;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;turbo_stream&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;remove&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;helpers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@todo&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;_container&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;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;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;todos_path&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 todo status.&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we updated the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_todo&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before_action&lt;/code&gt; to set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@todo&lt;/code&gt; instance variable when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change_status&lt;/code&gt; is called and we defined &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change_status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change_status&lt;/code&gt; updates the status of the given todo and then removes that that todo from the DOM. This will work for marking todos as complete and for marking them as incomplete — either way, we just target the id of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;li&amp;gt;&lt;/code&gt; and use a Turbo Stream &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;remove&lt;/code&gt; action.&lt;/p&gt;

&lt;p&gt;We added this new action because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; action we used in the first implementation of this feature use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; Turbo Stream action, instead of removing the todo from the DOM. We could have hacked the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; action to handle status changes differently or created a whole new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodoStatusChangesController&lt;/code&gt; for this, but there’s no reason to do that in our learning application.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/routes.rb&lt;/code&gt; to add the new route to the application:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:todos&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:change_status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;on: :member&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And finally, update the todo partial one last time to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change_status_todo_path&lt;/code&gt; on the status toggle buttons:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;complete?&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;n&quot;&gt;change_status_todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &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;s1&quot;&gt;&apos;incomplete&apos;&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;bg-green-600 px-4 py-2 rounded hover:bg-green-700&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :patch&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;sr-only&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Mark as incomplete&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 text-white&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;na&quot;&gt;fill=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&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;fill-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;clip-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&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;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;else&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;n&quot;&gt;change_status_todo_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;todo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;todo: &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;s1&quot;&gt;&apos;complete&apos;&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;bg-gray-400 px-4 py-2 rounded hover:bg-green-700&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :patch&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;sr-only&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Mark as complete&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 text-white&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;na&quot;&gt;fill=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&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;fill-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;clip-rule=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;evenodd&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With that last change in place, you can refresh the page and see that todos are now grouped into tabs. Toggle the status on a few todos and see that they are removed from the list of todos. Change tabs and see that the todo list updates:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-11-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application adding and removing todo items from a list.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Our application is so small that there’s no real way to tell that changing tabs is only updating the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos-container&lt;/code&gt; Turbo Frame, right? It could just be updating the whole page and we would never be able to tell. A quick way to test the Turbo Frame out (and to see why partial page updates can be so useful) is to add a dummy input to the page, outside of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;todos-container&lt;/code&gt; frame:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-11-dummy-input.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application entering text into an input and then navigating a list of todos while the input text stays in place.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Neat!&lt;/p&gt;

&lt;h2 id=&quot;degrading-gracefully&quot;&gt;Degrading gracefully&lt;/h2&gt;
&lt;p&gt;Throughout this application, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;respond_to&lt;/code&gt; blocks to render responses to Turbo Stream requests. The nice thing about this approach is that we always have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format.html&lt;/code&gt; back up ready to go if a user does not have JavaScript enabled in their browser.&lt;/p&gt;

&lt;p&gt;Because we constructed our application in this way, the application remains fully functional even when JavaScript is disabled. The partial page updates powered by Turbo give way to regular full page turns. Turbo applications, constructed thoughtfully, tend to rely less on JavaScript to function, making it easier to build applications that can gracefully fall back to normal, server rendered HTML and full page turns.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-11-disabled-js.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user of a web application disabling JavaScript in their browser and then using the todo application without any issues&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Depending on your application’s audience, this may not be a top priority, but Turbo-powered applications give you a solid base to build from if your application needs to serve JavaScript-free users.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we built a Turbo-powered todo application, using Turbo Streams and Turbo Frames to make fast, efficient page updates in response to user actions.&lt;/p&gt;

&lt;p&gt;This simple application served as a base to explore the basics of Turbo Streams and Turbo Frames and gave us a chance to debunk a few common misconceptions about Turbo Streams in the process.&lt;/p&gt;

&lt;p&gt;As you move forward in your Turbo journey, remember that Turbo Streams are for responding to form submissions. Streams give you the tools to update one or many elements after a form submission. You can render Turbo Streams inline in a controller, or from views.&lt;/p&gt;

&lt;p&gt;Turbo Frames are for scoping GET requests to a single piece of the page. Use Turbo Frames to add tabbed content to a page, to power search and filter interfaces, or for inline editing like we saw today. Turbo Frames always replace the entire content of the target frame, and only one frame can be updated per GET request.&lt;/p&gt;

&lt;p&gt;If you need more sophisticated update behavior (like appending items) or you need to update multiple elements at once, you cannot (easily) use a Turbo Frame.&lt;/p&gt;

&lt;p&gt;In this tutorial, we looked at basic use cases for Streams and Frames; however, we only just scratched the surface of what you can do with Turbo.&lt;/p&gt;

&lt;p&gt;To continue learning, a thorough review of the Turbo &lt;a href=&quot;https://turbo.hotwired.dev/reference/drive&quot;&gt;reference documentation&lt;/a&gt; is a good starting point. In particular, familiarizing yourself with the &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;events&lt;/a&gt; Turbo emits is important for more advanced use cases. Understanding Turbo Frame options is also important — functionality like &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#eager-loaded-frame&quot;&gt;eager&lt;/a&gt; and &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#lazy-loaded-frame&quot;&gt;lazy loaded&lt;/a&gt; frames, &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets&quot;&gt;breaking out&lt;/a&gt; of frames, and &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets&quot;&gt;targeting frames&lt;/a&gt; from the outside all help unlock powerful Turbo Frame-powered experiences.&lt;/p&gt;

&lt;p&gt;In addition to the Turbo documentation, you might find my more in-depth explorations of using &lt;a href=&quot;https://www.colby.so/posts/turbo-streams-on-rails&quot;&gt;Turbo Streams&lt;/a&gt; and &lt;a href=&quot;https://www.colby.so/posts/turbo-frames-on-rails&quot;&gt;Turbo Frames&lt;/a&gt; in Rails useful.&lt;/p&gt;

&lt;p&gt;For a much deeper dive into building a real application with Turbo, &lt;a href=&quot;https://twitter.com/alexandre_ruban&quot;&gt;Alexandre Ruban’s&lt;/a&gt; (in-progress) &lt;a href=&quot;https://www.hotrails.dev/&quot;&gt;Hotrails course&lt;/a&gt; is a great resource.&lt;/p&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Pagination and infinite scrolling with Rails and the Hotwire stack</title>
   <link href="https://colby.so/posts/pagination-and-infinite-scrolling-with-hotwire"/>
   <updated>2022-02-04T00:00:00+00:00</updated>
   <id>https://colby.so/posts/pagination-and-infinite-scrolling-with-hotwire</id>
   <content type="html">&lt;p&gt;&lt;em&gt;September 2022 Update: The techniques described in this article still work. However, with the release of Turbo 7.2, there is a simpler approach than the technique used in this article. You can find an updated version of this article &lt;a href=&quot;https://www.colby.so/posts/infinite-scroll-with-turbo-streams-and-stimulus&quot;&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nearly every web application will eventually need to add pagination to improve page load times and allow users to process information in a more consumable way — you don’t want to load 1,000 records in one request!&lt;/p&gt;

&lt;p&gt;Today, we are going to use the &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire stack&lt;/a&gt; (Turbo and Stimulus) to implement pagination in a Ruby on Rails application. We will implement pagination in three different ways, to give ourselves a chance to explore Turbo Frames, Turbo Streams, and Stimulus.&lt;/p&gt;

&lt;p&gt;This article was inspired by a conversation on the StimulusReflex discord and the &lt;a href=&quot;https://dev.to/dalezak/load-more-pagination-in-rails-with-hotwire-turbo-streams-433e&quot;&gt;great article&lt;/a&gt; by Dale Zak published as a result of that conversation.&lt;/p&gt;

&lt;p&gt;In Dale’s article, a purpose-built Stimulus controller is used to respond to a GET request with a Turbo Stream template. After reading that article, I decided to explore another method for achieving the same result, which is what we will tackle today.&lt;/p&gt;

&lt;p&gt;In the article, we will start with a simple Rails 7 application, build standard pagination with Pagy, and then layer on three different implementations of Turbo-powered pagination:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Pagination with Previous and Next page buttons&lt;/li&gt;
  &lt;li&gt;Manual “infinite scroll” with a load more button&lt;/li&gt;
  &lt;li&gt;Automatic infinite scroll&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we are finished, the infinite scroll version will look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-04-auto-infinite.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page that displays a list of widgets. As the user scrolls, new widgets are appended to the bottom of the list.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Before we begin, this article assumes that you are comfortable with Ruby on Rails and you have had a bit of exposure to Turbo and Stimulus. The techniques described in this article will work without Ruby on Rails, but the code will be easiest to follow if you are comfortable developing simple Ruby on Rails applications.&lt;/p&gt;

&lt;p&gt;You can find the complete code for this tutorial &lt;a href=&quot;https://github.com/DavidColby/turbo-pagination&quot;&gt;on Github&lt;/a&gt;, and you can try out a “production” version of the application &lt;a href=&quot;https://guarded-lake-91167.herokuapp.com/&quot;&gt;on Heroku&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s get started!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application setup&lt;/h2&gt;
&lt;p&gt;We will work from a new Rails 7 application, using importmap-rails to manage JavaScript and Tailwind for styling.&lt;/p&gt;

&lt;p&gt;Create a new Rails 7 application from your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new turbo-pagination &lt;span class=&quot;nt&quot;&gt;--css&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;tailwind
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;turbo-pagination&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To demonstrate pagination, we will create a simple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Widget&lt;/code&gt; resource. From your terminal again, use the built-in scaffold generator:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g scaffold Widget name:string
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Because we are using Tailwind via the &lt;a href=&quot;https://github.com/rails/tailwindcss-rails&quot;&gt;tailwindcss-rails gem&lt;/a&gt;, the scaffold generator applies some basic Tailwind styling to generated views, so we have nice looking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Widget&lt;/code&gt; pages right out of the box.&lt;/p&gt;

&lt;p&gt;In order to test pagination as we work, we will need some &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Widgets&lt;/code&gt; in the database. Open your rails console with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails c&lt;/code&gt; and add test data to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Widgets&lt;/code&gt; table:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;nf&quot;&gt;times&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;n&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Widget&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;Widget #&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;pagination-the-old-fashioned-way&quot;&gt;Pagination the old fashioned way&lt;/h2&gt;
&lt;p&gt;We are going to start by implementing pagination with standard Rails techniques. Each time a user requests a new page, we will load the new page with a full page turn, no Turbo required. Once pagination is working with full-page turns, we will add in Turbo to enhance the experience.&lt;/p&gt;

&lt;p&gt;In our application, we will use &lt;a href=&quot;https://github.com/ddnexus/pagy&quot;&gt;Pagy&lt;/a&gt; to implement pagination. Let’s install Pagy now, following along with the Pagy &lt;a href=&quot;https://ddnexus.github.io/pagy/how-to#quick-start&amp;amp;gsc.tab=0&quot;&gt;quick start guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;From your terminal, add pagy to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle add pagy&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Add Pagy’s backend to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Backend&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Add Pagy’s frontend helpers to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/application_helper.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ApplicationHelper&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Frontend&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With Pagy installed and ready to use across the application, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/widgets_controller.rb&lt;/code&gt; to paginate records on the index page:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@widgets&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;no&quot;&gt;Widget&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then finish up our traditional pagination implementation by adding a simple pager UI to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/widgets/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-full&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Widgets&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New widget&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_widget_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;widgets&quot;&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;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;@widgets&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;pager&quot;&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 my-8 flex justify-between&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;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;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prev&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;s2&quot;&gt;&quot;&amp;lt; Previous page&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;widgets_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prev&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;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;text-right&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;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;s2&quot;&gt;&quot;Next page &amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;widgets_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;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&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pager&lt;/code&gt; div and its contents, with a next and previous page buttons that render when a page exists to navigate to. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prev&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next&lt;/code&gt; methods used on the links are supplied by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pagy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With this change in place, boot up the rails application with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; and head to &lt;a href=&quot;http://localhost:3000/widgets&quot;&gt;http://localhost:3000/widgets&lt;/a&gt; and see that widgets are paginated at 10 items per page. Click the next and previous links to move between pages as desired. Notice that each time a paging button is clicked, a full page turn is initiated and the entire content of the page is replaced.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-04-full-page-turns.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page that displays a list of widgets. The user clicks buttons to move to the next page of widgets and the previous page of widgets. Each click reloads the page and scrolls the user to the very top of the browser window.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the next section, we will adjust our paging functionality to update only the widgets list and the pagination buttons, instead of performing a full-page turn on each request.&lt;/p&gt;

&lt;h2 id=&quot;navigate-pages-with-turbo&quot;&gt;Navigate pages with Turbo&lt;/h2&gt;
&lt;p&gt;In this section, we will use a &lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames&quot;&gt;Turbo Frame&lt;/a&gt; to update the content of the widgets area with the new page data. Turbo Frames allow us to scope navigation to specific part of the page instead of replacing the entire page with each request.&lt;/p&gt;

&lt;p&gt;Scoped navigation with Turbo Frames speeds up requests and allows us to build UIs that feel modern and fast, while continuing to use server-rendered HTML for each request.&lt;/p&gt;

&lt;p&gt;To begin, we will wrap the widgets list and the pagination controls in a Turbo Frame. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/widgets/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-full&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Widgets&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New widget&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_widget_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;s2&quot;&gt;&quot;widgets&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;min-w-full&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;@widgets&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;s2&quot;&gt;&quot;pager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;pagy: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we replaced the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;widgets&lt;/code&gt; div with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt;, and moved the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pager&lt;/code&gt; into that Turbo Frame.&lt;/p&gt;

&lt;p&gt;This change means that all link clicks within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;widgets&lt;/code&gt; Turbo Frame will now expect to receive a matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame&lt;/code&gt; response from the server. Turbo will then replace the content of that frame with the content supplied by the server, leaving the rest of the page content untouched.&lt;/p&gt;

&lt;p&gt;Before this will work, we need to add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pager&lt;/code&gt; partial and move the pagination controls into that partial. We don’t technically need to use a partial to render the pagination controls, but it helps keep the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; page readable.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/widgets/_pager.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then move the paging controls into the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pager&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;pager&quot;&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 my-8 flex justify-between&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;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;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prev&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;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt; Previous page&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;widgets_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &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;nf&quot;&gt;prev&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;turbo_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;advance&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;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;text-right&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;n&quot;&gt;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Next page &amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;widgets_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &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;nf&quot;&gt;next&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;turbo_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;advance&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;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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The content here is nearly identical to what was previously in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; page. The only change is the addition of a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-action&lt;/code&gt; data attribute on each link.&lt;/p&gt;

&lt;p&gt;By default, when navigating within a Turbo Frame, the page URL does not change. Normally, this is correct behavior navigation within a Turbo Frame, but in our case it is not.&lt;/p&gt;

&lt;p&gt;When a user moves from page one to page two, they expect to be able to refresh the page and stay on page two and to be able to use the back button in their browser to get back to page one.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#frame-that-promotes-navigations-to-visits&quot;&gt;advance value&lt;/a&gt; for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-action&lt;/code&gt; attribute tells Turbo to update the current URL and insert the previous page URL into the browser’s history, retaining the intuitive forward, back, and refresh behavior users expect.&lt;/p&gt;

&lt;p&gt;At this point, refresh &lt;a href=&quot;http://localhost:3000/widgets&quot;&gt;/widgets&lt;/a&gt; and see that clicking the Previous and Next page buttons correctly updates the content of the widgets frame. When you do this, you will notice one issue — navigating between pages does not update the user’s scroll position. They have to manually scroll back up to the top of the list to see the results.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-04-no-scrolling.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page that displays a list of widgets. The user clicks buttons to move to next and previous pages. With each click, the list of widgets updates but the user stays in the same scroll postiion, cutting off the top of the list of widgets.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We can fix this issue by updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;widgets&lt;/code&gt; Turbo Frame in the widgets index view:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;widgets&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;min-w-full&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;autoscroll: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;true&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#html-attributes&quot;&gt;autoscroll attribute&lt;/a&gt; tells Turbo to scroll the frame into view when the frame is loaded, automatically scrolling us back up to the top of the frame when a new page is loaded.&lt;/p&gt;

&lt;p&gt;Nice work so far! We now have standard pagination implemented, powered by Turbo Frames. In the next section, we’ll transition to a manual version of an infinite scroll experience.&lt;/p&gt;

&lt;h2 id=&quot;manual-infinite-scroll&quot;&gt;Manual “infinite scroll”&lt;/h2&gt;
&lt;p&gt;The first version of “infinite scroll” in our application will replace the Next and Previous pagination controls with a single load more button. When the user clicks this button, we will fetch the next page of widgets from the server, append them to the existing list of widgets, and update the load more button to prepare to fetch the next set of records.&lt;/p&gt;

&lt;p&gt;The major functional change is that instead of replacing the content of the widgets list with entirely new content, we need to keep the current widgets in the list and add the new widgets to the end of the list.&lt;/p&gt;

&lt;p&gt;This change will introduce us to a limitation of Turbo Frames. Today, navigation within a Turbo Frame always replaces the entire content of the Frame with new content. There is no concept of appending content using Turbo Frames — its replace or nothing.&lt;/p&gt;

&lt;p&gt;This means that to implement an infinite scroll experience, we need to reach for &lt;a href=&quot;https://turbo.hotwired.dev/handbook/streams&quot;&gt;Turbo Streams&lt;/a&gt;. In contrast to Turbo Frames, which always replace the target content, Turbo Streams can replace, remove, append, and prepend content as desired.&lt;/p&gt;

&lt;p&gt;Our goal is to use the pagination controls to retrieve new widgets from the server and then append those widgets to the existing list with Turbo Streams. When we are finished, our server will render &lt;a href=&quot;https://turbo.hotwired.dev/handbook/streams#stream-messages-and-actions&quot;&gt;turbo-stream elements&lt;/a&gt; as HTML, which Turbo will use to update the widgets list and the pagination controls without touching the rest of the page.&lt;/p&gt;

&lt;p&gt;To complicate matters a bit, Turbo &lt;a href=&quot;https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses&quot;&gt;expects&lt;/a&gt; Turbo Streams to be used with non-GET requests (like form submissions). There is no built-in way to render a Turbo Stream in response to a GET request, like the requests generated by clicks on our pagination controls.&lt;/p&gt;

&lt;p&gt;One way to work around this is described in &lt;a href=&quot;https://dev.to/dalezak/load-more-pagination-in-rails-with-hotwire-turbo-streams-433e&quot;&gt;Dale’s article&lt;/a&gt;. In it, a Stimulus controller and &lt;a href=&quot;https://github.com/rails/request.js&quot;&gt;request.js&lt;/a&gt; are used to insert a Turbo Stream header into GET requests, getting Turbo to see the request as a Turbo Stream request despite not originating from a form submission.&lt;/p&gt;

&lt;p&gt;The approach is Dale’s article is a completely valid way to solve the problem and it works quite well. However, we are going to use a different method to reach the same destination. Our approach will use a not-obvious but built-in Turbo behavior to get a Turbo Stream response without modifying headers.&lt;/p&gt;

&lt;p&gt;Whew. Let’s look at some code.&lt;/p&gt;

&lt;p&gt;To start, we need an empty Turbo Frame. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/widgets/index.html.erb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;w-full&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;n&quot;&gt;notice&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;cp&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;py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;notice&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;notice&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;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;h1&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-4xl&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Widgets&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New widget&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_widget_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;rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium&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;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;s2&quot;&gt;&quot;page_handler&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;widgets&quot;&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;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;@widgets&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;pagy: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; Turbo Frame with no content inside and we removed the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;widgets&lt;/code&gt; Turbo Frame, which we no longer need.&lt;/p&gt;

&lt;p&gt;This empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; frame will be the messenger that sneaks our Turbo Stream content in from the server, no header modification required.&lt;/p&gt;

&lt;p&gt;To see this in action, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pager&lt;/code&gt; partial to remove the old pagination controls and replace them with a single load more link:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;pager&quot;&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 my-8 flex justify-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;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;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Load more widgets&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;widgets_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &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;nf&quot;&gt;next&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;turbo_frame: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;page_handler&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;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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Notice the load more link is targeting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; Turbo Frame, &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets&quot;&gt;informing Turbo&lt;/a&gt; that clicks on that link should replace the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; frame, instead of navigating the entire page. Because the load more link is not nested within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; frame, we need this attribute to target that frame.&lt;/p&gt;

&lt;p&gt;Now we have pagination controls targeting an empty Turbo Frame, but clicking on the link will just re-render &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/widgets/index.html.erb&lt;/code&gt; with an empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; frame. That’s not very useful.&lt;/p&gt;

&lt;p&gt;To make this work, we need to update our controller to enable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame&lt;/code&gt; variants, so that we can render different content from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action in response to a Turbo Frame request.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Pagy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Backend&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:turbo_frame_request_variant&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;turbo_frame_request_variant&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;variant&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:turbo_frame&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_request?&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we are using a turbo-rails method, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_request?&lt;/code&gt;, to identify inbound Turbo Frame requests. When the inbound request is a Turbo Frame, we tell our controller to respond with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame&lt;/code&gt; &lt;a href=&quot;https://guides.rubyonrails.org/layouts_and_rendering.html#the-variants-option&quot;&gt;variant&lt;/a&gt; instead of the normal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;html.erb&lt;/code&gt; content.&lt;/p&gt;

&lt;p&gt;To see this in action, create the new Turbo Frame variant for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/widgets/index.html+turbo_frame.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then fill the new view in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;page_handler&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;turbo_stream_action_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;append&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;widgets&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;template: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@widgets&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sx&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;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_stream_action_tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;replace&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;pager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;template: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;pager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;pagy: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@pagy&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sx&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we respond with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; Turbo Frame because Turbo expects us to render content for that frame when the load more link is clicked.&lt;/p&gt;

&lt;p&gt;Inside of that Turbo Frame is where the magic happens. We first render a &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/2a2a5ec12e4233cf33e5d3f0a6855834f4e5e4ec/app/helpers/turbo/streams/action_helper.rb#L12&quot;&gt;Turbo Stream&lt;/a&gt; that appends the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@widgets&lt;/code&gt; to the existing list of widgets (using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;widgets&lt;/code&gt; id). Then we render another Turbo Stream to replace the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pager&lt;/code&gt; div with an updated version of the pager.&lt;/p&gt;

&lt;p&gt;Now, when the user clicks the load more link, a Turbo Frame request is sent to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/widgets&lt;/code&gt;, Rails sees the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html+turbo_frame.erb&lt;/code&gt; view and responds with the content of that view, rendered as plain HTML.&lt;/p&gt;

&lt;p&gt;Turbo then sees the response on client-side, “replaces” the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page_handler&lt;/code&gt; Turbo Frame tag with the two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-stream&lt;/code&gt; elements, and then processes the actions defined in those turbo-streams. The end result is a new set of widgets appended to the list, and a load more button updated to fetch the next page of results.&lt;/p&gt;

&lt;p&gt;See this in action by heading to the widgets index page and clicking the load more button. If all has gone well, each click of the load more button will append more widgets to the list and increment the page number each time.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-04-manual-infinite.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page that displays a list of widgets. The user clicks a load more button at the botttom of the list of widgets and a new set of widgets are appeneded to the bottom of the list.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Note that the Turbo Frame + Turbo Stream technique we used here was originally found on the &lt;a href=&quot;https://discuss.hotwired.dev/t/tip-turbo-stream-with-http-responses-including-get/3393&quot;&gt;Turbo discussion forums&lt;/a&gt; — the folks there figured it out, I’m just building on their great work.&lt;/p&gt;

&lt;p&gt;Now we have a manual “infinite scroll” experience in place. Let’s finish this article by using Stimulus to fetch new widgets automatically as the user scrolls down the page.&lt;/p&gt;

&lt;h2 id=&quot;automatic-infinite-scroll&quot;&gt;Automatic infinite scroll&lt;/h2&gt;
&lt;p&gt;Our infinite scroll experience will be powered by a Stimulus controller and will rely on the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver&quot;&gt;IntersectionObserver&lt;/a&gt; API to fetch new widgets automatically as the user scrolls the page.&lt;/p&gt;

&lt;p&gt;To make using the IntersectionObserver API easier, we will add the wonderful &lt;a href=&quot;https://github.com/stimulus-use/stimulus-use&quot;&gt;stimulus-use&lt;/a&gt; package to our application. This is not a requirement, but it does simplify the code a bit.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bin/importmap pin stimulus-use&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We also need a Stimulus controller to add the automatic fetch behavior to the DOM as the user scrolls. Again from your terminal, generate a new Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g stimulus autoclick&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill in the new Stimulus controller at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/autoclick_controller.js&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;useIntersection&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;stimulus-use&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;nc&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;options&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;threshold&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;nf&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;nf&quot;&gt;useIntersection&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;options&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;appear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entry&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;click&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This controller pulls in the &lt;a href=&quot;https://github.com/stimulus-use/stimulus-use/blob/main/docs/use-intersection.md&quot;&gt;useIntersection&lt;/a&gt; from stimulus-use. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appear&lt;/code&gt; function is triggered when the element the controller is attached scrolls into view in a user’s browser. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;appear&lt;/code&gt; simply calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;click()&lt;/code&gt; on the element the controller is attached to.&lt;/p&gt;

&lt;p&gt;To use this controller, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pager&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;pager&quot;&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 my-8 flex justify-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;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;pagy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;next&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;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s2&quot;&gt;&quot;Load more widgets&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;widgets_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;page: &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;nf&quot;&gt;next&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;rounded py-3 px-5 bg-gray-600 text-white block hover:bg-gray-800&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;turbo_frame: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;page_handler&quot;&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;autoclick&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;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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;autoclick&quot;&lt;/code&gt; to the load more link. With this change in place, each time the load more link is scrolled into view, the Stimulus controller will programmatically click the load more link. Each time this occurs, a Turbo Frame request to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action is fired to fetch and append the next set of widgets.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-04-auto-infinite.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page that displays a list of widgets. As the user scrolls, new widgets are appended to the bottom of the list.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;autoclick&lt;/code&gt; controller we are using here was lightly adapted from Sean Doyle’s &lt;a href=&quot;https://github.com/thoughtbot/hotwire-example-template/blob/ed84f0aa74481fbd116f0e66a4801417c9acb40e/app/javascript/controllers/autoclick_controller.js&quot;&gt;autoclick controller&lt;/a&gt; in his own implementation of infinite scrolling with Turbo.&lt;/p&gt;

&lt;p&gt;Sean’s implementation of infinite scrolling presents yet another approach to working around the limits of Turbo Frames and is worth reviewing in full, if you are interested in more advanced Turbo use cases. In Sean’s work, the key thing to note is his use of the code from this Turbo draft PR which adds additional “&lt;a href=&quot;https://github.com/hotwired/turbo/pull/146&quot;&gt;actions&lt;/a&gt;” to Turbo Frames.&lt;/p&gt;

&lt;p&gt;If you plan to use an approach like this one in a production application: A reader surfaced that the infinite scrolling technique we use in this book does not work reliably on Firefox for Android. Those users have to click the Load More button manually to load new records.&lt;/p&gt;

&lt;p&gt;That’s all for this tutorial, great work following along!&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we implemented multiple pagination styles in a Rails 7 application with Turbo Frames, Turbo Streams, and Stimulus. While building pagination, we got to see a couple of useful, more advanced uses of Turbo Frames in Rails:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Rendering Turbo Frame variants to respond with different content in response to Turbo Frame requests&lt;/li&gt;
  &lt;li&gt;Rendering Turbo Streams inside of empty Turbo Frame tags to use Turbo Streams in response to GET requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These types of techniques dramatically expand the usefulness of Turbo without adding significant complexity to your code, and are helpful tools to add to your Turbo kit.&lt;/p&gt;

&lt;p&gt;The very smart folks on the &lt;a href=&quot;https://discord.gg/stimulus-reflex&quot;&gt;StimulusReflex discord&lt;/a&gt; inspired this article, and the excellent work done by &lt;a href=&quot;https://twitter.com/dalezak&quot;&gt;Dale Zak&lt;/a&gt; and &lt;a href=&quot;https://github.com/seanpdoyle&quot;&gt;Sean Doyle&lt;/a&gt; served as a great foundation to build on.&lt;/p&gt;

&lt;p&gt;This article is intended to serve as a supplement to their work, presenting alternative approaches to help expand the set of tools we have to work with in Turbo.&lt;/p&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Real-time previews with Rails and StimulusReflex</title>
   <link href="https://colby.so/posts/real-time-previews-with-stimulus-reflex"/>
   <updated>2022-02-03T00:00:00+00:00</updated>
   <id>https://colby.so/posts/real-time-previews-with-stimulus-reflex</id>
   <content type="html">&lt;p&gt;Today we are going to build a live, server-rendered, liquid-enabled markdown previewer with Ruby on Rails and StimulusReflex. It’ll be pretty neat.&lt;/p&gt;

&lt;p&gt;Allowing users to preview their content before saving it is a common need in web applications — posts, products, emails. Any user-created content that gets turned into HTML can benefit from a preview function to help users check their work before they save it.&lt;/p&gt;

&lt;p&gt;Our StimulusReflex-powered previewer will parse user-generated markdown on the server, insert dynamic content with liquid, and update the DOM in ~100ms, fast enough that the preview feels instant.&lt;/p&gt;

&lt;p&gt;When our work is done, the end product will look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-03-liquid-parsing.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing into a form on a web page. As they type, what they type is displayed in a box beside the form input, with markdown formatting applied.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;If you’d like to try out the previewer in a production environment, you can &lt;a href=&quot;https://post-previewer-md.herokuapp.com/&quot;&gt;try it out on Heroku&lt;/a&gt;. The demo application is hosted on Heroku’s free tier so expect a delay on the first page load!&lt;/p&gt;

&lt;p&gt;Before diving in, this tutorial assumes a basic level of Rails knowledge. If you have never written Rails before, this tutorial may not be for you. You do not need any prior experience with Stimulus or StimulusReflex to follow along — in fact, this tutorial will be most useful to you if you are new to StimulusReflex and curious about how it can help you build great experiences faster.&lt;/p&gt;

&lt;p&gt;Let’s get started!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application setup&lt;/h2&gt;
&lt;p&gt;We are working from a base Rails 7 application with TailwindCSS and StimulusReflex installed. To follow along with this tutorial, start by cloning &lt;a href=&quot;https://github.com/DavidColby/stimulus_reflex_previews&quot;&gt;the starter repo&lt;/a&gt; so we can skip past the install steps and get to build the application. All of the code changes we will make in this tutorial can be found in &lt;a href=&quot;https://github.com/DavidColby/stimulus_reflex_previews/pull/1&quot;&gt;this pull request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our application will allow users to create and edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Posts&lt;/code&gt;. As they modify a post, the preview display beside the post will update. Before we can preview posts, we’ll need a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Post&lt;/code&gt; resource to work with, so let’s begin by scaffolding that resource up.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g scaffold Post title:string body:text
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;After you scaffold the resource, start up the rails application and build CSS and JavaScript with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt;. Head to &lt;a href=&quot;http://localhost:3000/posts&quot;&gt;http://localhost:3000/posts&lt;/a&gt; and make sure that creating and editing posts works before moving on.&lt;/p&gt;

&lt;p&gt;As a reminder, our end goal is a live, updated-as-you-type preview displayed beside the post form. We’ll construct the UI first. Create a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; partial from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/posts/_preview.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill the new partial in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;bg-gray-200 p-4 shadow-sm rounded-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;article&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;prose&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-2xl 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;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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&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;cp&quot;&gt;&amp;lt;%=&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;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;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/article&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/posts/new.html.erb&lt;/code&gt; to render the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; partial beside the post form, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mx-auto md:w-2/3 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;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-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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;preview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/posts/edit.html.erb&lt;/code&gt; with the same content:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mx-auto md:w-2/3 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;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-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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;preview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we could have created a new partial with the content we added to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt;, but it is not strictly necessary. Feel free to move this content to a partial if you prefer.&lt;/p&gt;

&lt;p&gt;Now we have preview content displayed beside the post form, but the preview content is static — visit &lt;a href=&quot;http://localhost:3000/posts/new&quot;&gt;posts/new&lt;/a&gt; and see that the “preview” is just an empty gray box. Not very useful yet.&lt;/p&gt;

&lt;p&gt;In the next section, we will create a StimulusReflex-enabled Stimulus controller to update the preview content as the user types, making the gray preview box a little more useful.&lt;/p&gt;

&lt;h2 id=&quot;adding-a-stimulus-controller&quot;&gt;Adding a Stimulus controller&lt;/h2&gt;
&lt;p&gt;Our application will eventually rely on a server-side reflex to update the content of the preview partial that we created in the last section. However, before we do that, let’s build a purely client-side implementation of the preview function to ease into things.&lt;/p&gt;

&lt;p&gt;From your terminal, use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulus_reflex&lt;/code&gt; generator:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g stimulus_reflex post
rails stimulus:manifest:update&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;After this generator runs, you will see that the generator created both a client-side Stimulus controller and a server-side reflex: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/post_controller.js&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/reflexes/post_reflex.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the client-side, StimulusReflex enhances vanilla Stimulus controllers. StimulusReflex-powered Stimulus controllers have all of the same functionality of a regular Stimulus controller, with extra functionality layered on top.&lt;/p&gt;

&lt;p&gt;To demonstrate this, we are going to start by using the new Stimulus controller, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post_controller.js&lt;/code&gt; to update the content of the post preview as the user types. We will not have any markdown formatting or liquid substitution, but the page will react to user input and we will get to see a few Stimulus concepts in action.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/post_controller.js&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&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;./application_controller&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;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&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;title&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;body&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;titlePreview&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;bodyPreview&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;nf&quot;&gt;preview&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;titlePreviewTarget&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;titleTarget&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bodyPreviewTarget&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bodyTarget&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;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we inherit from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;, the base StimulusReflex controller.&lt;/p&gt;

&lt;p&gt;In the controller, we define &lt;a href=&quot;https://stimulus.hotwired.dev/reference/targets&quot;&gt;targets&lt;/a&gt; that we will use to obtain an easy reference to the elements that we care about. We use these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targets&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; function. Each time &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; is called, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;titlePreview&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bodyPreview&lt;/code&gt; elements are updated with the current value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;titleTarget&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bodyTarget&lt;/code&gt;, respectively.&lt;/p&gt;

&lt;p&gt;For the most part, this is regular JavaScript. If we wanted to, we could remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targets&lt;/code&gt; and instead select each element by an id, with something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.getElementById(&apos;body-target&apos;)&lt;/code&gt;. Stimulus targets give us a more convenient method to access any number of elements by a name, but nothing magical is happening.&lt;/p&gt;

&lt;p&gt;To use this new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; function, we need to connect the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostController&lt;/code&gt; to the DOM and add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt; elements and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actions&lt;/code&gt; to the preview and form partials.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mx-auto md:w-2/3 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;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-12&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;post&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;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;post-preview&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;preview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;post&quot;&lt;/code&gt; to the div wrapping the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; partials.&lt;/p&gt;

&lt;p&gt;To use a Stimulus controller, you must connect the controller to a DOM element with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller&lt;/code&gt; attribute. Stimulus &lt;a href=&quot;https://stimulus.hotwired.dev/reference/controllers#scopes&quot;&gt;scopes controllers&lt;/a&gt; based on the DOM hierarchy — the element with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller&lt;/code&gt; attribute and all of the children of that element will be within that controller’s scope. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targets&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actions&lt;/code&gt; for a controller must be within the scope to function.&lt;/p&gt;

&lt;p&gt;This scoping mechanism allows developers to have multiple independent instances of the same controller present on the page at once, when needed.&lt;/p&gt;

&lt;p&gt;Update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; partial next:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &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;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contents&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;k&quot;&gt;if&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;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error_explanation&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3&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&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluralize&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;errors&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error&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; prohibited this post from being saved:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&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;errors&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;error&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;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-5&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:title&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full&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;post_target: &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;ss&quot;&gt;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;input-&amp;gt;post#preview&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;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;my-5&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_area&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;:body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;rows: &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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full&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;post_target: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;body&quot;&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;input-&amp;gt;post#preview&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;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;inline&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The important updates here are on the two form inputs. On each, we added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-post-target&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; attribute. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post-target&lt;/code&gt; attribute defines a target for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostController&lt;/code&gt;, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;action&lt;/code&gt; attribute tells Stimulus to fire the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostController&apos;s&lt;/code&gt; preview function when an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; event occurs on that input.&lt;/p&gt;

&lt;p&gt;With this change in place, each time a user types a character in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt; inputs, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostController&lt;/code&gt; will run the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; function to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;titlePreview&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bodyPreview&lt;/code&gt; elements.&lt;/p&gt;

&lt;p&gt;This won’t work just yet though. Before it will, we need to set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;titlePreview&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bodyPreview&lt;/code&gt; targets. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/posts/_preview.html.erb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;bg-gray-200 p-4 shadow-sm rounded-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;article&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;prose&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-2xl font-bold&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-post-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;titlePreview&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;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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-post-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bodyPreview&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;post&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;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/article&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Notice the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-post-target&lt;/code&gt; attributes on the header and the body tags.&lt;/p&gt;

&lt;p&gt;With this change in place, refresh the new post page, start typing and see that the content of the preview updates as you type.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-03-stimulus-version.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing into a form on a web page. Everything they type is copied into a box beside the form&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is a great start, but we aren’t really “previewing” anything because the Stimulus controller is not parsing markdown content or substituting liquid tags.&lt;/p&gt;

&lt;p&gt;To add useful previews, we will use the server-side &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostReflex&lt;/code&gt; that we generated at the beginning of this section.&lt;/p&gt;

&lt;h2 id=&quot;server-side-previews-with-stimulusreflex&quot;&gt;Server-side previews with StimulusReflex&lt;/h2&gt;
&lt;p&gt;Stimulus controllers do their work on the client-side, adding and removing elements, updating classes or attributes. Even when we are using StimulusReflex, this is still true — Stimulus controllers stay on the client-side. To add server-side functionality, we need to move to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostReflex&lt;/code&gt;, at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/reflexes/post_reflex.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflex-classes&quot;&gt;reflex class&lt;/a&gt; will be responsible for transforming the content of the preview body from markdown to HTML, and for substituting any liquid tags present in the content. Once the content is transformed, StimulusReflex will send the transformed content back to the client, where it will be inserted seamlessly into the DOM, all in ~100ms.&lt;/p&gt;

&lt;p&gt;Before adding markdown and liquid parsing, let’s move the client-side preview updates to the server-side reflex. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostReflex&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PostReflex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationReflex&lt;/span&gt;  
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;preview&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#preview-title&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post_params&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;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#preview-body&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:body&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;post_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;:post&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;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In this reflex, we are using two &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs&quot;&gt;selector morphs&lt;/a&gt; to update the preview content, identifying the element to update with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;. Selector morphs allow us to run reflex actions without a full pass through a controller action with ActionDispatch, and are perfect for features that only update a small piece of the page, like our preview functionality.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post_params&lt;/code&gt; is identical to attribute whitelisting that you’ll find in a standard Rails controller — each time this reflex is called, the data from the post form will be serialized and sent to the server, giving us a handy way to reference the current content of the form.&lt;/p&gt;

&lt;p&gt;In StimulusReflex, there are two ways to &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflexes&quot;&gt;call a server-side reflex&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The first option is through a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reflex&lt;/code&gt; data attribute on a DOM element.  &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflexes#declaring-a-reflex-in-html-with-data-attributes&quot;&gt;This approach&lt;/a&gt; completely bypasses the reflex’s related Stimulus controller and directly invokes the server-side reflex. This option works well when you have a simple reflex that does not need custom options to function.&lt;/p&gt;

&lt;p&gt;Our use case requires us to use the second method for calling reflexes: using &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflexes#calling-a-reflex-in-a-stimulus-controller&quot;&gt;this.stimulate&lt;/a&gt; in a Stimulus controller. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulate&lt;/code&gt; is &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflexes#stimulate-is-extremely-flexible&quot;&gt;extremely flexible&lt;/a&gt; and allows us to override the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt; passed to the server-side reflex, define options, and run any JavaScript we like before sending the reflex to the server.&lt;/p&gt;

&lt;p&gt;To call a reflex with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulate&lt;/code&gt;, head back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/post_controller.js&lt;/code&gt; and update it like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&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;./application_controller&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;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nf&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;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;nf&quot;&gt;preview&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;nf&quot;&gt;stimulate&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;Post#preview&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;na&quot;&gt;serializeForm&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, we removed the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targets&lt;/code&gt; from the controller and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; now just calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.stimulate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;serializeForm&lt;/code&gt; &lt;a href=&quot;https://docs.stimulusreflex.com/v/v3.5/rtfm/working-with-forms#stimulusreflex-form-processing&quot;&gt;option&lt;/a&gt; ensures the content of the closest &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; element in the DOM is passed to the reflex on the server. The data in the form is accessible on the server via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt;, as we saw in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostReflex&lt;/code&gt; earlier in this section.&lt;/p&gt;

&lt;p&gt;Now we have a reflex defined on the server, and a Stimulus controller ready to call that reflex. To make this work, we need to update the DOM to match the structure expected by the reflex.&lt;/p&gt;

&lt;p&gt;Recall in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; method in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostReflex&lt;/code&gt;, we have two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morphs&lt;/code&gt; that rely on ids to target the right element in the DOM:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;preview&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#preview-title&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post_params&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;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#preview-body&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:body&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;For the reflex to work, we need an element with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview-title&lt;/code&gt; id and another with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview-body&lt;/code&gt; id. Update the preview partial like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;bg-gray-200 p-4 shadow-sm rounded-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;article&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;prose&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;preview-title&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-2xl 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;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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;preview-body&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;post&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;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/article&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now both the title element and the body element have ids that match the expectations of the reflex, so when the reflex runs, those elements will be updated.&lt;/p&gt;

&lt;p&gt;Now move over to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; partial to connect the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post&lt;/code&gt; Stimulus controller to the form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &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;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contents&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;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;post&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;k&quot;&gt;if&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;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error_explanation&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3&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&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluralize&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;errors&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error&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; prohibited this post from being saved:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&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;errors&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;error&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;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;my-5&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:title&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full&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;input-&amp;gt;post#preview&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;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;my-5&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_area&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;:body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;rows: &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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full&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;input-&amp;gt;post#preview&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;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;inline&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, notice the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; element now has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;post&quot;&lt;/code&gt; attribute, scoping the Stimulus controller to the form. We also updated the form inputs to remove the unnecessary &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-post-target&lt;/code&gt; attributes.&lt;/p&gt;

&lt;p&gt;Because we are serializing the entire content of the form, we no longer need a direct reference to the individual input elements in the Stimulus controller, so we do not need the target attributes.&lt;/p&gt;

&lt;p&gt;One last step to move the preview functionality to the server. Because we connected the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostController&lt;/code&gt; to the form, we need to remove the controller from the wrapper div in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/view/posts/new.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mx-auto md:w-2/3 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;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-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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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;w-1/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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;preview&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;post: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@post&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Previously, we needed a direct reference in the Stimulus controller to the preview elements, so we needed both the form and preview partials to be within the scope of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because the preview elements are now updated in the server-side reflex and referenced by id, the preview partial no longer needs to be in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostController’s&lt;/code&gt; scope.&lt;/p&gt;

&lt;p&gt;With this last change in place, head back to &lt;a href=&quot;http://localhost:3000/posts/new&quot;&gt;http://localhost:3000/posts/new&lt;/a&gt;, start typing and see that the preview content updates each time the form changes. To confirm that StimulusReflex is doing the work on the server, watch the Rails server logs as you type. With each key press, the server logs will output information about the reflex:&lt;/p&gt;

&lt;p&gt;T&lt;img src=&quot;/public/images/2022-02-03-log-output.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing into a form on a web page with a terminal window display server logs below the browser. As the user types, the characters they type are copied into a preview box and the server logs indicate that the StimulusReflex post#preview action has run.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now we are “previewing” content as the user types, but that preview still is not doing anything useful. We just added a round trip to the server, but the server is not doing anything useful with the content yet.&lt;/p&gt;

&lt;p&gt;In the next section, we will make the server-side reflex more valuable by adding markdown and liquid tag parsing when the preview content is updated.&lt;/p&gt;

&lt;h2 id=&quot;add-markdown-and-liquid-parsing&quot;&gt;Add markdown and liquid parsing&lt;/h2&gt;
&lt;p&gt;We will use &lt;a href=&quot;https://github.com/vmg/redcarpet&quot;&gt;redcarpet&lt;/a&gt; to parse markdown. To do so, We need to add redcarpet to our application.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle add redcarpet&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Restart your Rails application and head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/posts_helper.rb&lt;/code&gt; and add a method to parse markdown content:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PostsHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;to_markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&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;s1&quot;&gt;&apos;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&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;n&quot;&gt;markdown&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&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;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Render&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTML&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;fenced_code_blocks: &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;autolink: &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;markdown&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;content&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This helper takes a string, initializes a new instance of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Redcarpet::Markdown&lt;/code&gt; as described in the &lt;a href=&quot;https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use&quot;&gt;documentation&lt;/a&gt;, and then renders the content to HTML.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_markdown&lt;/code&gt; helper will be used in two places in our application: When the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; partial is loaded during a normal request (like when visiting the post edit page) and when parsing content on the fly in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostReflex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; partial first, at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/posts/_preview.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;bg-gray-200 p-4 shadow-sm rounded-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;article&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;prose&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-2xl font-bold&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;preview-title&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;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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;preview-body&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;to_markdown&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;body&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;/article&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When a user visits the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt; pages, markdown content in the body will be parsed and rendered as HTML.&lt;/p&gt;

&lt;p&gt;Update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostReflex&lt;/code&gt; to use the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_markdown&lt;/code&gt; helper:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;preview&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#preview-title&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post_params&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;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#preview-body&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;helpers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:body&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt; now, type some markdown content in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt; input and see that as you type, the markdown is transformed into HTML instantly.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-03-markdown-parsing.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing into a form on a web page. As they type, what they type is displayed in a box beside the form input, with markdown formatting applied.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;At this point, we have server-powered markdown parsing — nice work!&lt;/p&gt;

&lt;p&gt;Our next step is adding &lt;a href=&quot;https://github.com/Shopify/liquid&quot;&gt;liquid&lt;/a&gt; support, allowing users to transform &lt;a href=&quot;https://shopify.github.io/liquid/basics/introduction/&quot;&gt;liquid content&lt;/a&gt; into real data and see that content processed in real time.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle add liquid&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Back to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PostsHelper&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PostsHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;to_markdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&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;s1&quot;&gt;&apos;&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&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;n&quot;&gt;markdown&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Markdown&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;no&quot;&gt;Redcarpet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Render&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;HTML&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;fenced_code_blocks: &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;autolink: &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;markdown&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;liquified&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&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;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;liquified&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&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;content&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;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;company_name&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Hotwiring Rails&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;liquified&lt;/code&gt; takes a string of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content&lt;/code&gt;, scans it for matching liquid tags and objects, and translates them.&lt;/p&gt;

&lt;p&gt;Our example implementation mocks up a simple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;company_name&lt;/code&gt; tag — in a real application we could use liquid tags to insert data about the object we are working on.&lt;/p&gt;

&lt;p&gt;Restart your Rails application again and then go back to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit &lt;/code&gt;posts page, enter in a body with some markdown content and the `` tag and see that the tags are replaced as you type:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2022-02-03-liquid-parsing.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing into a form on a web page. As they type, what they type is displayed in a box beside the form input, with markdown formatting applied.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;debouncing-requests&quot;&gt;Debouncing requests&lt;/h2&gt;
&lt;p&gt;Right now, every keystroke triggers a round trip to the server. Many of these requests are unnecessary because the user will have typed another character before the request completes. A simple way to reduce the load on the server is to debounce &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; function, waiting for the user to pause before triggering the server-side reflex.&lt;/p&gt;

&lt;p&gt;To debounce the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; function, we will use lodash’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;debounce&lt;/code&gt;. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;yarn add lodash&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Post&lt;/code&gt; Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&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;./application_controller&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;debounce&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;lodash/debounce&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nf&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;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;connect&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;preview&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;debounce&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;preview&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;mi&quot;&gt;50&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;preview&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;nf&quot;&gt;stimulate&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;Post#preview&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;na&quot;&gt;serializeForm&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preview&lt;/code&gt; will wait 50ms before firing. Feel free to play around with the wait period to find the time that feels right to you.&lt;/p&gt;

&lt;p&gt;And with that change, you have reached the end of this tutorial, great work today!&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we built a fully functional, StimulusReflex-powered markdown and liquid tag parser. Our application processes content on the server and returns it to the client without page turns, saving records in the database, or dealing with the overhead of a Rails controller action.&lt;/p&gt;

&lt;p&gt;While building this application, we learned a bit about how to use Stimulus and StimulusReflex in Rails application and applied some basic techniques of both to create a real-time experience while staying close to core Rails principles.&lt;/p&gt;

&lt;p&gt;Before using something like what we built today in production, a couple of things to think about:&lt;/p&gt;

&lt;h3 id=&quot;why-not-just-parse-things-on-the-client&quot;&gt;Why not just parse things on the client?&lt;/h3&gt;
&lt;p&gt;Throughout this tutorial, you may have wondered “Why don’t we just parse the markdown &lt;a href=&quot;https://github.com/markedjs/marked&quot;&gt;on the client&lt;/a&gt;?”&lt;/p&gt;

&lt;p&gt;Liquid processing was introduced to give us a reason to parse content like this on the server. We are here to learn about Stimulus and StimulusReflex, so I fit the requirements to that goal.&lt;/p&gt;

&lt;p&gt;If your application just needs markdown parsing without the extra complications of liquid, a client-side parser is a completely reasonable (and more performant) choice!&lt;/p&gt;

&lt;h3 id=&quot;do-you-need-real-time-previews&quot;&gt;Do you need real-time previews?&lt;/h3&gt;
&lt;p&gt;The live preview we built is useful for learning and showing off what StimulusReflex can do, but it may not be the most desirable user experience in a real application. The live preview experience works fine for some use cases, particularly when the content is only a few paragraphs in length.&lt;/p&gt;

&lt;p&gt;As the content gets longer, a better approach is moving the “preview” content into a separate tab, hidden by default. As the user types, update the content in the hidden tab as usual, but keep the content hidden until the user requests it. That experience is likely to scale a bit better, while not being any more technically complex than the experience we built.&lt;/p&gt;

&lt;h3 id=&quot;further-reading&quot;&gt;Further reading&lt;/h3&gt;
&lt;p&gt;The best resources for learning about Stimulus are the official &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;handbook&lt;/a&gt; and &lt;a href=&quot;https://stimulus.hotwired.dev/reference/controllers&quot;&gt;reference&lt;/a&gt;. The official documentation does not cover more advanced use cases and best practices. For that, &lt;a href=&quot;https://www.betterstimulus.com/&quot;&gt;BetterStimulus&lt;/a&gt; is a great starting point.&lt;/p&gt;

&lt;p&gt;To learn more about StimulusReflex, &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;the documentation&lt;/a&gt; is exceptional, and is the best place to start your journey. The &lt;a href=&quot;https://discord.gg/stimulus-reflex&quot;&gt;StimulusReflex discord&lt;/a&gt; is also a wonderful resource full of kind and helpful folks.&lt;/p&gt;

&lt;p&gt;That’s all for today. As always, thank you for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Hotwiring Rails Newsletter - January 2022</title>
   <link href="https://colby.so/posts/hotwiring-rails-january-2022"/>
   <updated>2022-02-01T00:00:00+00:00</updated>
   <id>https://colby.so/posts/hotwiring-rails-january-2022</id>
   <content type="html">&lt;p&gt;This newsletter went out via email to the Hotwiring Rails subscriber list, but I know that not everyone wants another email cluttering up their inbox. For those folks, I’ll always publish the full content of the newletter here too, so you can get all the content with none of the emails.&lt;/p&gt;

&lt;p&gt;Thoughts or feedback on how I can make this newsletter more valuable? Have something you’d like to include in next month’s edition? Send me an &lt;a href=&quot;mailto:david@colby.so?subject=Hotwiring+Rails&quot;&gt;email&lt;/a&gt;, or find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#articles-and-guides&quot;&gt;Articles and guides&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;articles-and-guides&quot;&gt;Articles and guides&lt;/h2&gt;

&lt;h3 id=&quot;the-hotwire-example-treasure-trove-by-sean-doyle&quot;&gt;The Hotwire example treasure trove by Sean Doyle&lt;/h3&gt;

&lt;p&gt;This Github repo has been making the Twitter and Discord rounds lately, and for good reason.&lt;/p&gt;

&lt;p&gt;Each branch in the repo contains a writeup and full implementation of a common UX pattern built with Stimulus and Turbo. Keep this handy and reference the examples when you need them.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/thoughtbot/hotwire-example-template/branches/active&quot;&gt;View the treasure trove on Github&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;live-reloading-for-hotwire-rails-apps-by-kirill-platonov&quot;&gt;Live reloading for Hotwire Rails apps by Kirill Platonov&lt;/h3&gt;

&lt;p&gt;With the demise of Webpacker (more on that below), replacing webpack-dev-server is top of mind for many folks. Kirill explored this problem and built a &lt;a href=&quot;https://github.com/kirillplatonov/hotwire-livereload&quot;&gt;gem&lt;/a&gt; to tackle it.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://kirillplatonov.com/posts/hotwire-livereload/&quot;&gt;Read the how-to on kirillplatonov.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;remote-modals-with-rails-hotwire-and-bootstrap-by-benito-serna&quot;&gt;Remote modals with Rails, Hotwire, and Bootstrap by Benito Serna&lt;/h3&gt;

&lt;p&gt;This quick article offers a slightly different take on implementing server-rendered modal content with Turbo, hooking into the before-render callback to neatly transition the modal out when needed.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://bhserna.com/remote-modals-with-rails-hotwire-and-bootstrap.html&quot;&gt;Read the full article on bhserna.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;pagination-in-rails-from-scratch-by-ana-nunes-da-silva&quot;&gt;Pagination in Rails from scratch by Ana Nunes da Silva&lt;/h3&gt;

&lt;p&gt;Implementing common application needs from scratch can be a really interesting learning exercise. It can be worth doing even if just to get more comfortable reasoning about the problem before reaching for a battle-tested gem.&lt;/p&gt;

&lt;p&gt;In this article, you’ll build a simple pagination system from scratch, complete with a front end implementation and the standard set of convenience methods.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.ananunesdasilva.com/posts/pagination-in-rails-from-scratch-no-gems&quot;&gt;Build your own pagination on ananunesdasilva.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Speaking of building things from scratch, ICYMI, Steve Polito’s &lt;a href=&quot;https://stevepolito.design/blog/rails-authentication-from-scratch/&quot;&gt;recent look&lt;/a&gt; at building an authentication system from scratch is a good read.&lt;/p&gt;

&lt;h3 id=&quot;dueling-tutorials-two-ways-to-lazy-load-tooltips-with-modern-rails&quot;&gt;Dueling tutorials: Two ways to lazy load tooltips with modern Rails&lt;/h3&gt;

&lt;p&gt;Steve Polito published &lt;a href=&quot;https://thoughtbot.com/blog/hotwire-asynchronously-loaded-tooltips&quot;&gt;“Hotwire: Asynchronously loaded tooltips”&lt;/a&gt; last week based on code from Sean Doyle. This is a well-constructed article guiding you through remotely loaded tooltips powered by Turbo Frames. As a bonus, it includes a nice use of TailwindCSS’s &lt;a href=&quot;https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-sibling-state&quot;&gt;peer&lt;/a&gt; class.&lt;/p&gt;

&lt;p&gt;Julian Rubisch read Steve’s article and was inspired to &lt;a href=&quot;https://blog.minthesize.com/tooltips-with-futurism&quot;&gt;write up an alternative implementation&lt;/a&gt; of the same functionality with &lt;a href=&quot;https://github.com/stimulusreflex/futurism&quot;&gt;Futurism&lt;/a&gt;. As Julian notes in the article, Futurism can offer a simpler migration path for legacy applications that were not designed from zero with Turbo Frames in mind.&lt;/p&gt;

&lt;p&gt;Sometimes we can get stuck in one single path forward — Julian’s response article is a great reminder that there are usually alternatives out there when we need them.&lt;/p&gt;

&lt;h3 id=&quot;on-stimulus-reflex-why-and-why-by-stephen-margheim&quot;&gt;On Stimulus Reflex: Why and Why by Stephen Margheim&lt;/h3&gt;

&lt;p&gt;This is a thoughtful, code-free read from Stephen. Inspired by conversations on the &lt;a href=&quot;https://discord.com/invite/d3zeZ5Uc?utm_campaign=Hotwiring+Rails&amp;amp;utm_medium=email&amp;amp;utm_source=Revue+newsletter&quot;&gt;StimulusReflex Discord&lt;/a&gt;, Stephen presents a take on how StimulusReflex fits into a Turbo-powered Rails world.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://fractaledmind.com/articles/on-stimulus-reflex/&quot;&gt;Read it on fractaledmind.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;rails-7-features-before-and-after-by-zakaria-fatahi&quot;&gt;Rails 7 features, before and after by Zakaria Fatahi&lt;/h3&gt;

&lt;p&gt;This gist is a nice, compact introduction to some of the most interesting new features in Rails 7. A nice bookmark to read when you’re making the Rails 7 upgrade.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://gist.github.com/zakariaf/534ff8dfc3a807779133dc078114b969&quot;&gt;View it on Github&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/h2&gt;

&lt;h3 id=&quot;farewell-webpacker&quot;&gt;Farewell, Webpacker&lt;/h3&gt;

&lt;p&gt;Webpacker was &lt;a href=&quot;https://github.com/rails/webpacker/commit/16bba5d6ca862b950a002f19c10d12e4bf51a87b#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5&quot;&gt;officially retired&lt;/a&gt; in January. Sadly, it was retired before an official release of Webpacker 6 was cut.&lt;/p&gt;

&lt;p&gt;Rails 7’s import maps and js/cssbundling gems made Webpacker’s retirement a foregone conclusion, but it was surprising to see it retired without a 6.0 release.&lt;/p&gt;

&lt;p&gt;For folks on Webpacker 5.4, despite the retirement I do not think there is an urgent need to switch off of Webpacker — you can still upgrade existing apps to Rails 7 without changing bundling strategies.&lt;/p&gt;

&lt;p&gt;For folks who love Webpacker and want to stick with it long term, Webpacker’s development has moved to &lt;a href=&quot;https://github.com/shakacode/shakapacker&quot;&gt;Shakapacker&lt;/a&gt;, which is likely to be maintained well into the future.&lt;/p&gt;

&lt;h3 id=&quot;custom-turbo-stream-actions&quot;&gt;Custom Turbo Stream actions&lt;/h3&gt;

&lt;p&gt;This is technically a comment on a PR, but its my newsletter and I make the rules. Sean Doyle stars again, this time demonstrating a method for “adding” new actions to Turbo Streams through clever use template tags and Stimulus.&lt;/p&gt;

&lt;p&gt;If you need more power than Turbo Streams seven actions offer (and you can’t use &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/&quot;&gt;CableReady&lt;/a&gt;), this technique might help.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/hotwired/turbo/pull/479#issuecomment-999683061&quot;&gt;View the technique on Github&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/h2&gt;

&lt;p&gt;This is the third Hotwiring Rails newsletter I’ve written, and Baldur’s writing has been in the best thing(s) I’ve read recently twice.&lt;/p&gt;

&lt;p&gt;Apologies for the lack of variety but &lt;a href=&quot;https://www.baldurbjarnason.com/2022/i-cant-keep-up-with-web-dev/&quot;&gt;this piece&lt;/a&gt; was a great read on why keeping up with the latest and greatest in web development can feel so overwhelming compared to the same problem in other fields.
Baldur’s article is well-worth reading for anyone feeling overwhelmed by trying to keep up with all the newest tools, tricks, and techniques in web development.&lt;/p&gt;

&lt;p&gt;In particular, shifting from “keeping up” to doing research is a great mental model for cutting through the noise and keeping yourself focused on resources that are useful for you and interesting to you.
This specific article is a funny thing to share in a newsletter about the latest Rails developments — the whole deal is that I share a bunch of Rails-related content because who doesn’t love learning as much as possible all of the time, right?&lt;/p&gt;

&lt;p&gt;Putting the article into context with this newsletter: I resonate with Baldur’s “do research” approach and I model my own content consumption in a similar way.&lt;/p&gt;

&lt;p&gt;In Baldur’s terms, my “Work Question” is what’s happening at the edge of Ruby on Rails. Most of the content I consume is only useful to me in that context, and that’s where I keep my brain while I’m reading.
Before organizing myself around this Work Question, I spent a lot of time trying to keep up with React and Vue, and I spent hours following tutorials on building serverless apps (yikes) — things I had no real interest in or need for, but that seemed like I should be aware of.&lt;/p&gt;

&lt;p&gt;One day I just stopped reading all of that stuff. I have no idea what’s going on with React, or Vue, or even if those are still the cool JavaScript frameworks to use. Instead, I just learn about what’s going on in Rails, and I’m happier and better informed because of it.&lt;/p&gt;

&lt;p&gt;If you are deep in information overload, read Baldur’s piece for help and then apply his approach to this newsletter — if an article I linked doesn’t fit your Work Question, do you need to read it or can you ignore it?&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.baldurbjarnason.com/2022/i-cant-keep-up-with-web-dev/&quot;&gt;Read it on baldurbjarnason.com&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;until-next-time&quot;&gt;Until next time&lt;/h2&gt;

&lt;p&gt;RailsConf 2022’s &lt;a href=&quot;https://cfp.rubycentral.org/events/railsconf-2022&quot;&gt;Call For Proposals&lt;/a&gt; is open — you have until February 28th to submit!&lt;/p&gt;

&lt;p&gt;Thanks for reading! Tune in next month for news on &lt;a href=&quot;https://twitter.com/davidcolbyatx/status/1458171481029021704&quot;&gt;Hotwired ATS&lt;/a&gt;.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Hotwiring Rails Newsletter - December 2021</title>
   <link href="https://colby.so/posts/hotwiring-rails-december-2021"/>
   <updated>2021-12-07T00:00:00+00:00</updated>
   <id>https://colby.so/posts/hotwiring-rails-december-2021</id>
   <content type="html">&lt;p&gt;This newsletter went out via email to the Hotwiring Rails subscriber list, but I know that not everyone wants another email cluttering up their inbox. For those folks, I’ll always publish the full content of the newletter here too, so you can get all the content with none of the emails.&lt;/p&gt;

&lt;p&gt;Thoughts or feedback on how I can make this newsletter more valuable? Have something you’d like to include in next month’s edition? Send me an &lt;a href=&quot;mailto:david@colby.so?subject=Hotwiring+Rails&quot;&gt;email&lt;/a&gt;, or find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#articles-and-guides&quot;&gt;Articles and guides&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#recent-pieces-from-my-blog&quot;&gt;Recent pieces from my blog&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;articles-and-guides&quot;&gt;Articles and guides&lt;/h2&gt;
&lt;h3 id=&quot;rails-hotwire-cableready-and-stimulusreflex-are-bffs-by-nate-hopkins&quot;&gt;Rails, Hotwire, CableReady, and StimulusReflex are BFFs by Nate Hopkins&lt;/h3&gt;
&lt;p&gt;This article from the creator of CableReady and StimulusReflex offers perspective on how the different pieces of the modern Rails landscape fit together.&lt;/p&gt;

&lt;p&gt;This one is full of useful links and thoughtfully lays out a key philosophical distinction between Turbo’s aggressive adherence to RESTful principles and SR’s embrace of flexibility.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/hopsoft/rails-hotwire-cableready-and-stimulusreflex-are-bffs-4a89&quot;&gt;Read it on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;kubing-rails-stressless-kubernetes-deployments-with-kuby-by-vladimir-dementyev-and-travis-turner&quot;&gt;Kubing Rails: stressless Kubernetes deployments with Kuby by Vladimir Dementyev and Travis Turner&lt;/h3&gt;
&lt;p&gt;If you (like me) have been intimidated by the complexity of &lt;a href=&quot;https://kubernetes.io/&quot;&gt;Kubernetes&lt;/a&gt;, you might be interested in taking &lt;a href=&quot;https://getkuby.io/&quot;&gt;Kuby&lt;/a&gt; for a spin.&lt;/p&gt;

&lt;p&gt;In this detailed tutorial on deploying a real Rails application with Kuby, the Evil Martians team gets down in the weeds and describes challenges they encountered and how they overcame them while deploying a Rails + AnyCable application to DigitalOcean with Kuby.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://evilmartians.com/chronicles/kubing-rails-stressless-kubernetes-deployments-with-kuby&quot;&gt;Read it on evilmartians.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;why-hotwire-in-2021-by-cédric-fabianski-and-elizabeth-braae&quot;&gt;Why Hotwire in 2021 by Cédric Fabianski and Elizabeth Braae&lt;/h3&gt;
&lt;p&gt;The Bearer team talks about their transition from a complex React-powered frontend to a simplified, Hotwire-powered approach with a small team and a rapidly growing product.&lt;/p&gt;

&lt;p&gt;If you’re considering whether your team can make the switch, I’ll leave this quote from the article here, click through to read the rest:&lt;/p&gt;

&lt;p&gt;“We were essentially maintaining two applications where one should have been enough. The big learning was that this architecture might have made sense for large, stable applications, but it didn’t make sense for a company iterating fast to find a good product market fit.”&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.bearer.com/blog/why-hotwire&quot;&gt;Read it on bearer.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;building-a-real-time-chat-app-in-rails-using-actioncable-and-turbo-by-abiodun-ajibade&quot;&gt;Building a Real-Time Chat App in Rails Using ActionCable and Turbo by Abiodun Ajibade&lt;/h3&gt;
&lt;p&gt;This implementation of the classic chat application demo relies on Turbo Streams to build a multi-room chat application, with a little bit of Stimulus in the mix. Turbo Stream model broadcasts keep things simple and clean — a nice introduction to the power of Turbo Streams and, more generally, the power that ActionCable unlocks for Rails libraries.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.honeybadger.io/blog/chat-app-rails-actioncable-turbo/&quot;&gt;Read it on honeybadger.io&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;rails-remote-elements-tutorial-by-steve-polito&quot;&gt;Rails Remote Elements Tutorial by Steve Polito&lt;/h3&gt;
&lt;p&gt;Steve takes us through an interesting approach to implementing a SPA with rails-ujs and Stimulus, showing us that the tools to build highly responsive user experiences in Rails have been available for years.&lt;/p&gt;

&lt;p&gt;Leveraging Stimulus data attributes and built in rails-ujs events, we get a classic SPA todo application without React. An interesting experiment and learning experience, and a nice reminder of the power of rails-ujs, forgotten, but not gone.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stevepolito.design/blog/rails-remote-elements-tutorial/&quot;&gt;Read it on stevepolito.design&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;a-rails-multi-tenant-strategy-thats-30-lines-and-just-works-by-jason-meller&quot;&gt;A Rails Multi-Tenant Strategy That’s ~30 Lines and “Just Works” by Jason Meller&lt;/h3&gt;
&lt;p&gt;This quick read demonstrates a simple multi-tenant implementation without reliance on gems. Thoughtfully designed and battle tested in a large production application, this is a nice implementation of a common requirement for Rails-powered SaaS applications.&lt;/p&gt;

&lt;p&gt;Included is a neat trick for setting Current in the Rails console, discussions of testing considerations, and a thoughtful acknowledgement of criticisms of Rails’ Current attributes implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/kolide/a-rails-multi-tenant-strategy-thats-30-lines-and-just-works-58cd&quot;&gt;Read it on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;rails-7-railties-and-sorbet-at-shopify-with-rafael-frança-from-rails-core-by-remote-ruby&quot;&gt;Rails 7, Railties, and Sorbet at Shopify with Rafael França from Rails Core by Remote Ruby&lt;/h3&gt;
&lt;p&gt;I’ll confess that I’m not much for listening to technical podcasts, but this conversation between the Remote Ruby team and Rafael França was very enjoyable.&lt;/p&gt;

&lt;p&gt;Come for the (now confirmed) hint at the Rails 7 release timeline, stay for the thoughtful words from Rafael on Shopify’s development practices, types, and Hotwire.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://remoteruby.transistor.fm/157&quot;&gt;Listen on transistor.fm&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-cableready-language-implementation-project&quot;&gt;The CableReady Language Implementation Project&lt;/h3&gt;
&lt;p&gt;The StimulusReflex team has a vision to expand CableReady outside of the Rails world and into a “universal standard tool for developers to dynamically control browsers from the server”. This is a big project, and they’re looking for help from developers capable of building server-side libraries in languages like Go, Python, and Java. If this sounds like you, give their call for support a review and get in touch with the team.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/leastbad/the-cableready-language-implementation-project-4hjd&quot;&gt;Read it on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;new-and-interesting-prs-and-releases&quot;&gt;New and interesting PRs and releases&lt;/h2&gt;
&lt;h3 id=&quot;turbo&quot;&gt;Turbo&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/hotwired/turbo/releases/tag/v7.1.0&quot;&gt;Turbo 7.1 is out&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;This is a huge release that includes changes that adds rails-ujs style functionality to Turbo and fixes for a number of outstanding bugs, a few of the highlights in this release for me:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/hotwired/turbo/pull/398&quot;&gt;Push state history on frame navigations&lt;/a&gt; — This is the most exciting addition in 7.1 in my opinion, making it trivial to update URLs on frame navigation. This change dramatically improves the usability of tabbed interfaces and search forms powered by Turbo Frames&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/hotwired/turbo/pull/439&quot;&gt;Add requestSubmit polyfill&lt;/a&gt; — This small change eliminates the need to manually add the requestSubmit polyfill to projects using Turbo. If you’ve moved away from rails-ujs, you may have gotten bit by the lack of access to Rails.fire(‘submit’) like me. requestSubmit with this polyfill is a nice alternative&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/hotwired/turbo/pull/379&quot;&gt;Add data-confirm behavior to Turbo&lt;/a&gt; — Another rails-ujs behavior cleanly migrated to Turbo. The implementation is slightly more verbose so that it doesn’t clash with existing rails-ujs behavior, just in case your project has Turbo alongside old rails-ujs code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;rails&quot;&gt;Rails&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://weblog.rubyonrails.org/2021/12/6/Rails-7-0-rc-1-released/&quot;&gt;Rails 7 RC1 is out&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;With this release, we’re on track to see Rails 7.0 out before the end of the year.&lt;/p&gt;

&lt;p&gt;Since Alpha 2 in September, much of the work has been on bug fixes, deprecation, and documentation. One interesting PR that made the cut was the addition of &lt;a href=&quot;https://github.com/rails/rails/pull/43625&quot;&gt;a standardized error reporting interface&lt;/a&gt;, via Rails.error that should simplify handling and reporting errors in Rails apps.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;recent-pieces-from-my-blog&quot;&gt;Recent pieces from my blog&lt;/h2&gt;
&lt;p&gt;I won’t be publishing many brand new pieces for the next few months because I’m focused on finishing my &lt;a href=&quot;https://twitter.com/davidcolbyatx/status/1458171481029021704&quot;&gt;modern Rails development book/course&lt;/a&gt; (~2/3 done with the code).&lt;/p&gt;

&lt;p&gt;Before going heads down, I published &lt;a href=&quot;https://www.colby.so/posts/live-reloading-with-esbuild-and-rails&quot;&gt;this quick piece&lt;/a&gt; on my Rails + esbuild config for live reloads.&lt;/p&gt;

&lt;p&gt;I’m also committed to keeping my turbo-rails documentation pieces up to date with new Turbo releases — &lt;a href=&quot;https://www.colby.so/posts/turbo-frames-on-rails&quot;&gt;Turbo Frames on Rails&lt;/a&gt; was updated after the release of Turbo 7.1.&lt;/p&gt;

&lt;p&gt;Expect to see many more shorter form tutorials from me after I publish my Hotwiring Rails course in (fingers crossed) late January.
___&lt;/p&gt;

&lt;h2 id=&quot;the-best-things-i-read-recently&quot;&gt;The best things I read recently&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://shkspr.mobi/blog/2021/01/the-unreasonable-effectiveness-of-simple-html/&quot;&gt;The unreasonable effectiveness of simple HTML by Terence Eden&lt;/a&gt; — This newsletter, and all of my writing, is about modern web development. I’m interested in building highly interactive websites, which some times means using techniques absolutely will not work on Internet Explorer, much less on a PSP or a TV.&lt;/p&gt;

&lt;p&gt;This piece from January of 2021 is a thoughtful reminder that, for many use cases, your users are best served with simple, old school websites that work, all the time, in every situation, with no fluff.&lt;/p&gt;

&lt;p&gt;Exploring new things is fun, cool, and sometimes profitable, but if you’re building web sites for critical services, very broad audiences, or vulnerable populations, you probably don’t need that slick animation or cool new JavaScript tech.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://blog.pragmaticengineer.com/pragmatic-engineer-test/&quot;&gt;The pragmatic engineer test: 12 questions on engineering culture by Gergely Orosz&lt;/a&gt; — Hiring Rails developers seems to get harder by the day and retaining your current team can be a challenge.&lt;/p&gt;

&lt;p&gt;Building the right culture — and sharing that culture widely — can help you get a leg up. Gergely’s thoughtful overview of the questions you should be able to answer as a leader about your engineering team’s culture is a nice place to get started.&lt;/p&gt;

&lt;p&gt;Bonus — &lt;a href=&quot;https://twitter.com/ideasasylum/status/1456704035726237702&quot;&gt;Jamie Lawrence walks through&lt;/a&gt; how his team at Podia scores on these 12 questions.&lt;/p&gt;

&lt;h2 id=&quot;until-next-time&quot;&gt;Until next time&lt;/h2&gt;
&lt;p&gt;RailsConf 2022 &lt;a href=&quot;https://twitter.com/railsconf/status/1466450611541475334&quot;&gt;has been announced&lt;/a&gt;. I’ll be there — I hope to see some of you there too!&lt;/p&gt;

&lt;p&gt;That’s all for this month’s edition of Hotwiring Rails. For those of you who celebrate, happy holidays! I’ll be back in the new year.&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Live reloading with Ruby on Rails and esbuild</title>
   <link href="https://colby.so/posts/live-reloading-with-esbuild-and-rails"/>
   <updated>2021-11-06T00:00:00+00:00</updated>
   <id>https://colby.so/posts/live-reloading-with-esbuild-and-rails</id>
   <content type="html">&lt;p&gt;As you may have heard by now, Rails 7 comes out of the box with &lt;a href=&quot;https://github.com/rails/importmap-rails&quot;&gt;importmap-rails&lt;/a&gt; and the mighty &lt;a href=&quot;https://github.com/rails/webpacker&quot;&gt;Webpacker&lt;/a&gt; is no longer the default for new Rails applications.&lt;/p&gt;

&lt;p&gt;For those who aren’t ready to switch to import maps and don’t want to use Webpacker now that it is no longer a Rails default, &lt;a href=&quot;https://github.com/rails/jsbundling-rails&quot;&gt;jsbundling-rails&lt;/a&gt; was created. This gem adds the option to use &lt;a href=&quot;https://webpack.js.org/&quot;&gt;webpack&lt;/a&gt;, &lt;a href=&quot;https://rollupjs.org/guide/en/&quot;&gt;rollup&lt;/a&gt;, or &lt;a href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt; to bundle JavaScript while using the asset pipeline to deliver the bundled files.&lt;/p&gt;

&lt;p&gt;Of the three JavaScript bundling options, the Rails community seems to be most interested in using esbuild, which aims to bring about a “new era of build tool performance” and offers extremely fast build times and enough features for most users’ needs.&lt;/p&gt;

&lt;p&gt;Using esbuild with Rails, via jsbundling-rails is very simple, especially in a new Rails 7 application; however, the default esbuild configuration is missing a few quality of life features. Most important among these missing features is live reloading. Out of the box, each time you change a file, you need to refresh the page to see your changes.&lt;/p&gt;

&lt;p&gt;Once you’ve gotten used to live reloading (or its fancier cousin, &lt;a href=&quot;https://webpack.js.org/concepts/hot-module-replacement/&quot;&gt;Hot Module Replacement&lt;/a&gt;), losing it is tough.&lt;/p&gt;

&lt;p&gt;Today, esbuild &lt;a href=&quot;https://github.com/evanw/esbuild/issues/645#issuecomment-755215007&quot;&gt;doesn’t support HMR&lt;/a&gt;, but with some effort it is possible to configure esbuild to support live reloading via automatic page refreshing, and that’s what we’re going to do today.&lt;/p&gt;

&lt;p&gt;We’ll start from a fresh Rails 7 install and then modify esbuild to support live reloading when JavaScript, CSS, and HTML files change.&lt;/p&gt;

&lt;p&gt;Before we get started, please note that this very much an experiment that hasn’t been battle-tested. I’m hoping that this is a nice jumping off point for discussion and improvements. YMMV.&lt;/p&gt;

&lt;p&gt;With that disclaimer out of the way, let’s get started!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application setup&lt;/h2&gt;
&lt;p&gt;We’ll start by creating a new Rails 7 application.&lt;/p&gt;

&lt;p&gt;If you aren’t already using Rails 7 for new Rails applications locally, &lt;a href=&quot;https://www.aloucaslabs.com/miniposts/using-a-specific-rails-version-when-you-generate-a-new-rails-app-with-rails-new-command&quot;&gt;this article&lt;/a&gt; can help you get your local environment ready.&lt;/p&gt;

&lt;p&gt;Once your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails new&lt;/code&gt; command is ready for Rails 7, from your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new live_esbuild &lt;span class=&quot;nt&quot;&gt;-j&lt;/span&gt; esbuild
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;live_esbuild
rails db:create
rails g controller Home index&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we created a new Rails application set to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsbundling-rails&lt;/code&gt; with esbuild and then generated a controller we’ll use to verify that the esbuild configuration works.&lt;/p&gt;

&lt;h3 id=&quot;booting-up&quot;&gt;Booting up&lt;/h3&gt;
&lt;p&gt;In addition to installing esbuild for us, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsbundling-rails&lt;/code&gt; creates a few files that simplify starting the server and building assets for development. It also changes how you’ll boot up your Rails app locally.&lt;/p&gt;

&lt;p&gt;Rather than using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails s&lt;/code&gt;, you’ll use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; uses &lt;a href=&quot;https://github.com/ddollar/foreman&quot;&gt;foreman&lt;/a&gt; to run multiple start up scripts, via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Procfile.dev&lt;/code&gt;. We’ll make a change to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Procfile.dev&lt;/code&gt; later, but for now just know that when you’re ready to boot up your app, use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; to make sure your assets are built properly.&lt;/p&gt;

&lt;h2 id=&quot;configure-esbuild-for-live-reloading&quot;&gt;Configure esbuild for live reloading&lt;/h2&gt;
&lt;p&gt;To enable live reloading, we’ll start by creating an esbuild config file. From your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;esbuild-dev.config.js&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To make things a bit more consumable, we’ll first enable live reloading for JavaScript files only, leaving CSS and HTML changes to wait for manual page refreshes.&lt;/p&gt;

&lt;p&gt;We’ll add reloading for views and CSS next, but we’ll start simpler.&lt;/p&gt;

&lt;p&gt;To enable live reloading on JavaScript changes, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;esbuild-dev.config.js&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;cp&quot;&gt;#!/usr/bin/env node
&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;http&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;http&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;argv&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;--watch&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;clients&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watchOptions&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;onRebuild&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;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&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;if &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;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;error&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;Build failed:&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;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;else&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;nf&quot;&gt;log&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;Build succeeded&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;nx&quot;&gt;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&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;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&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;data: update&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&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;nx&quot;&gt;clients&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;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;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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;esbuild&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;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;entryPoints&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;application.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;na&quot;&gt;bundle&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;na&quot;&gt;outdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cwd&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;app/assets/builds&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;na&quot;&gt;absWorkingDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cwd&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;app/javascript&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;na&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watchOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;banner&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;js&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; (() =&amp;gt; new EventSource(&quot;http://localhost:8082&quot;).onmessage = () =&amp;gt; location.reload())();&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;span class=&quot;k&quot;&gt;catch&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;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exit&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;nx&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&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;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;writeHead&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Content-Type&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;text/event-stream&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;Cache-Control&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;no-cache&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;Access-Control-Allow-Origin&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;*&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;na&quot;&gt;Connection&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;keep-alive&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;nf&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8082&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There’s a lot going on here, let’s walk through it a section at a time:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;http&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;http&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;argv&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;--watch&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;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;clients&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;First we require packages and define a few variables, easy so far, right?&lt;/p&gt;

&lt;p&gt;Next, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watchOptions&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watchOptions&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;onRebuild&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;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&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;if &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;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;error&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;Build failed:&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;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;else&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;nf&quot;&gt;log&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;Build succeeded&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;nx&quot;&gt;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&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;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&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;data: update&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&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;nx&quot;&gt;clients&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;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;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watchOptions&lt;/code&gt; will be passed to esbuild to define what happens each time an esbuild rebuild is triggered.&lt;/p&gt;

&lt;p&gt;When there’s an error, we output the error, otherwise, we output a success message and then use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;res.write&lt;/code&gt; to send data out to each client.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clients.length = 0&lt;/code&gt; empties the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clients&lt;/code&gt; array to prepare it for the next rebuild.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;esbuild&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;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;entryPoints&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;application.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;na&quot;&gt;bundle&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;na&quot;&gt;outdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cwd&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;app/assets/builds&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;na&quot;&gt;absWorkingDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cwd&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;app/javascript&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;na&quot;&gt;watch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;watchOptions&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;banner&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;js&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; (() =&amp;gt; new EventSource(&quot;http://localhost:8082&quot;).onmessage = () =&amp;gt; location.reload())();&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;span class=&quot;k&quot;&gt;catch&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;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exit&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This section defines the esbuild &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; command, passing in &lt;a href=&quot;https://esbuild.github.io/api/#build-api&quot;&gt;the options&lt;/a&gt; we need to make our (JavaScript only) live reload work.&lt;/p&gt;

&lt;p&gt;The important options are the &lt;a href=&quot;https://esbuild.github.io/api/#watch&quot;&gt;watch option&lt;/a&gt;, which takes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watch&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watchOptions&lt;/code&gt; variables we defined earlier and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;banner&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;esbuild’s &lt;a href=&quot;https://esbuild.github.io/api/#banner&quot;&gt;banner option&lt;/a&gt; allows us to prepend arbitrary code to the JavaScript file built by esbuild. In this case, we insert an &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/EventSource&quot;&gt;EventSource&lt;/a&gt; that fires &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;location.reload()&lt;/code&gt; each time a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/EventSource/onmessage&quot;&gt;message is received&lt;/a&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:8082&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Inserting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EventSource&lt;/code&gt; banner and sending a new request from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8082&lt;/code&gt; each time &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rebuild&lt;/code&gt; runs is what enables live reloading for JavaScript files to work. Without the EventSource and the local request sent on each rebuild, we would need to refresh the page manually to see changes in our JavaScript files.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nx&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&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;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;writeHead&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Content-Type&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;text/event-stream&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;Cache-Control&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;no-cache&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;Access-Control-Allow-Origin&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;*&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;na&quot;&gt;Connection&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;keep-alive&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;nf&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8082&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This section at the end of the file simply starts up a local web server using node’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http&lt;/code&gt; &lt;a href=&quot;https://nodejs.dev/learn/the-nodejs-http-module&quot;&gt;module&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With the esbuild file updated, we need to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; to use the new config file:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot; data-lang=&quot;json&quot;&gt;&lt;span class=&quot;nl&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;esbuild app/javascript/*.* --bundle --outdir=app/assets/builds&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;node esbuild-dev.config.js&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we updated the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts&lt;/code&gt;  section of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; to add a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start&lt;/code&gt; script that uses our new config file. We’ve left &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; as-is since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; will be used on production deployments where our live reloading isn’t needed.&lt;/p&gt;

&lt;p&gt;Next, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Procfile.dev&lt;/code&gt; to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start&lt;/code&gt; script:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;web: bin/rails server &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 3000
js: yarn start &lt;span class=&quot;nt&quot;&gt;--watch&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, let’s make sure our JavaScript reloading works. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/home/index.html.erb&lt;/code&gt; to connect the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hello&lt;/code&gt; Stimulus controller:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Home#index&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Find me in app/views/home/index.html.erb&lt;span class=&quot;nt&quot;&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now boot up the app with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; and head to &lt;a href=&quot;http://localhost:3000/home/index&quot;&gt;http://localhost:3000/home/index&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then open up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/hello_controller.js&lt;/code&gt; and make a change to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;connect&lt;/code&gt; method, maybe something like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;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;nc&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;nf&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;element&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Hello Peter. What&apos;s happening?&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If all has gone well, you should see the new Hello Peter header on the page, replacing the Hello World header.&lt;/p&gt;

&lt;p&gt;If all you want is JavaScript live reloading, feel free to stop here. If you want live reloading for your HTML and CSS files, that’s where we’re heading next.&lt;/p&gt;

&lt;h2 id=&quot;html-and-css-live-reloading&quot;&gt;HTML and CSS live reloading&lt;/h2&gt;
&lt;p&gt;esbuild helpfully watches our JavaScript files and rebuilds every time they change. It doesn’t know anything about non-JS files, and so we’ll need to branch out a bit to get full live reloading in place.&lt;/p&gt;

&lt;p&gt;Our basic approach will be to scrap esbuild’s watch mechanism and replace it with our own file system monitoring that triggers rebuilds and pushes updates over the local server when needed.&lt;/p&gt;

&lt;p&gt;To start, we’re going to use &lt;a href=&quot;https://github.com/paulmillr/chokidar&quot;&gt;chokidar&lt;/a&gt; to watch our file system for changes, so that we can reload when we update a view or a CSS file, not just JavaScript files.&lt;/p&gt;

&lt;p&gt;Install chokidar from your terminal with:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;yarn add chokidar &lt;span class=&quot;nt&quot;&gt;-D&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With chokidar installed, we’ll update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;esbuild-dev.config.js&lt;/code&gt; again, like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;cp&quot;&gt;#!/usr/bin/env node
&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;path&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chokidar&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;chokidar&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;http&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;http&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;clients&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;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;createServer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;res&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;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;writeHead&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Content-Type&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;text/event-stream&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;Cache-Control&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;no-cache&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;Access-Control-Allow-Origin&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;*&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;na&quot;&gt;Connection&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;keep-alive&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;nf&quot;&gt;listen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8082&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;builder&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;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;esbuild&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;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints&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;application.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;na&quot;&gt;bundle&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;na&quot;&gt;outdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cwd&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;app/assets/builds&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;na&quot;&gt;absWorkingDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;nx&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cwd&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;app/javascript&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;na&quot;&gt;incremental&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;na&quot;&gt;banner&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;js&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; (() =&amp;gt; new EventSource(&quot;http://localhost:8082&quot;).onmessage = () =&amp;gt; location.reload())();&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;span class=&quot;nx&quot;&gt;chokidar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;watch&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;./app/javascript/**/*.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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./app/views/**/*.html.erb&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;./app/assets/stylesheets/*.css&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;nf&quot;&gt;on&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;all&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;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;javascript&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;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;rebuild&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;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&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;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&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;data: update&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&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;nx&quot;&gt;clients&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;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;nf&quot;&gt;builder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Again, lots going on here. Let’s step through the important bits.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;chokidar&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;chokidar&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;First, we require &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chokidar&lt;/code&gt;, which we need to setup file system watching. Starting easy again.&lt;/p&gt;

&lt;p&gt;Next, we setup the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; task:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;builder&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;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;esbuild&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;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// snip unchanged options&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;incremental&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;nx&quot;&gt;chokidar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;watch&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;./app/javascript/**/*.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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./app/views/**/*.html.erb&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;./app/assets/stylesheets/*.css&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;nf&quot;&gt;on&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;all&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;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;path&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;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;path&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;javascript&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;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;rebuild&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;clients&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;res&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;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&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;data: update&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n\n&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;nx&quot;&gt;clients&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve moved the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt; setup into an async function that assigns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;result&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We also added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;incremental&lt;/code&gt; &lt;a href=&quot;https://esbuild.github.io/api/#incremental&quot;&gt;flag&lt;/a&gt; to the builder, which makes repeated builds (which we’ll be doing) more efficient.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;watch&lt;/code&gt; option was removed since we no longer want esbuild to watch for changes on rebuild on its own.&lt;/p&gt;

&lt;p&gt;Next, we setup &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chokidar&lt;/code&gt; to watch files in the javascript, views, and stylesheets directories. When a change is detected, we check the path to see if the file was a javascript file. If it was, we manually trigger a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rebuild&lt;/code&gt; of our JavaScript.&lt;/p&gt;

&lt;p&gt;Finally, we send a request out from our local server, notifying the browser that it should reload the current page.&lt;/p&gt;

&lt;p&gt;With these changes in place, stop the server if it is running and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; again. Open up or refresh &lt;a href=&quot;http://localhost:3000/home/index&quot;&gt;http://localhost:3000/home/index&lt;/a&gt;, make changes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html.erb&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application.css&lt;/code&gt; and see that those changes trigger page reloads and that updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hello_controller.js&lt;/code&gt; still triggers a reload.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-11-06-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with a browser window and a code editor open, side-by-side. As the user makes changes in their code editor, the browser automatically refreshes and reflects the changes made in the editor.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we created an esbuild config file that enables live reloading (but not HMR) for our jsbundling-rails powered Rails application. As I mentioned at the beginning of this article, this is very much an experiment and this configuration has not been tested on an application of any meaningful size. You can find the finished code for this example application &lt;a href=&quot;https://github.com/DavidColby/esbuild-live-reload&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’m certain that there are better routes out there to the same end result, and I’d love to hear from others on pitfalls to watch out for and ways to improve my approach.&lt;/p&gt;

&lt;p&gt;While researching this problem, I leaned heavily on previous examples of esbuild configs. In particular, the examples found at these two links were very helpful in getting live reload to a functional state:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;This &lt;a href=&quot;https://github.com/rails/jsbundling-rails/issues/40#issuecomment-939514493&quot;&gt;example esbuild config&lt;/a&gt;, from an issue on the jsbundling-rails Github repo&lt;/li&gt;
  &lt;li&gt;This &lt;a href=&quot;https://github.com/evanw/esbuild/issues/802&quot;&gt;discussion&lt;/a&gt; on the esbuild Github repo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you, like me, are a Rails developer that needs to learn more about bundling and bundlers, a great starting point is &lt;a href=&quot;https://dev.to/paramagicdev/frontend-bundler-braindump-10fj&quot;&gt;this deep dive&lt;/a&gt; into the world of bundlers. If you’re intested in full HMR without any speed loss, and you’re willing to break out of the standard Rails offerings, you might enjoy &lt;a href=&quot;https://vite-ruby.netlify.app/&quot;&gt;vite-ruby&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, if you’re using esbuild with Rails and Stimulus, you’ll probably find the &lt;a href=&quot;https://github.com/excid3/esbuild-rails&quot;&gt;esbuild-rails plugin&lt;/a&gt; from Chris Oliver useful.&lt;/p&gt;

&lt;p&gt;That’s all for today. As always - thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Rendering view components with Turbo Stream broadcasts</title>
   <link href="https://colby.so/posts/rendering-view-components-with-turbo-stream-broadcasts"/>
   <updated>2021-11-03T00:00:00+00:00</updated>
   <id>https://colby.so/posts/rendering-view-components-with-turbo-stream-broadcasts</id>
   <content type="html">&lt;p&gt;View components, via Github’s &lt;a href=&quot;https://github.com/github/view_component&quot;&gt;view_component gem&lt;/a&gt;, are growing in popularity in the Rails community but until recently, view components and &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb&quot;&gt;Turbo Stream broadcasts&lt;/a&gt; didn’t play well together. This made using both view components and Turbo Streams in the same application clunky and a little frustrating.&lt;/p&gt;

&lt;p&gt;While there were &lt;a href=&quot;https://blog.kuda.dev/making-hotwire-play-nice-with-viewcomponent-ckoilssb40msdv9s10v139kgs&quot;&gt;ways&lt;/a&gt; to get components working with streams, thanks to a recent addition to &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;turbo-rails&lt;/a&gt;, rendering view components from Turbo Streams now works seamlessly out of the box.&lt;/p&gt;

&lt;p&gt;To demonstrate how to connect these two powerful tools together, we’ll be building a very simple Rails application that allows users to manage a list of Spies.&lt;/p&gt;

&lt;p&gt;Each spy in the list of spies will be rendered using a view component, and when new spies are added to the database, the newly created spy record will be rendered and broadcast via a callback in the spy model.&lt;/p&gt;

&lt;p&gt;When we’re finished, it’ll work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-11-03-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with two open browser windows. In one, a list of spies is shown. In the other, the user is typing into a form to create a new spy. They finish typing, click a button to create a spy, and the browser window the list of spies is updated to include the newly created spy&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Beautiful, I know.&lt;/p&gt;

&lt;p&gt;This article assumes that you’re comfortable building Rails applications. You won’t need any previous experience with Turbo Streams or view components to follow along.&lt;/p&gt;

&lt;p&gt;If you want to skip right to the end the complete code for this article can be found on &lt;a href=&quot;https://github.com/DavidColby/turbo-view-components/tree/broadcast-spies&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start building!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application setup&lt;/h2&gt;
&lt;p&gt;We’ll be working from a fresh Rails application. I’m working off of the latest Rails 7 release (alpha2 at the time of this writing) but everything in this tutorial will work fine on Rails 6.1 too.&lt;/p&gt;

&lt;p&gt;If you want to follow along, you can run the below commands from your terminal, or you can clone this &lt;a href=&quot;https://github.com/DavidColby/turbo-view-components&quot;&gt;Github repo&lt;/a&gt; and skip ahead to the &lt;a href=&quot;#building-the-spy-component&quot;&gt;Building the spy component section&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new turbo-view-components &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;turbo-view-components
rails g scaffold Spy name:string mission:string
bundle add view_component
rails db:create db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If you’re using Rails 6.1 instead of 7, you’ll also need to install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; manually.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;bundle add turbo-rails
rails turbo:install&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Whichever route you choose, you’re ready to move on to the next section once you have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view_component&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; installed in your application and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Spy&lt;/code&gt; resource created. Once you’re setup, start up your server with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails s&lt;/code&gt; and head to &lt;a href=&quot;http://localhost:3000/spies&quot;&gt;http://localhost:3000/spies&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;building-the-spy-component&quot;&gt;Building the spy component&lt;/h2&gt;
&lt;p&gt;To use view components, we need to create a component to render a spy object. We can create new components with the built-in &lt;a href=&quot;https://viewcomponent.org/guide/generators.html&quot;&gt;generator&lt;/a&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view_component&lt;/code&gt; provides:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails g component Spy spy&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This generator will create both a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_component.rb&lt;/code&gt; file and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_component.html.erb&lt;/code&gt; file in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;components&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;Since we’re building a very simple component, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_component.rb&lt;/code&gt; is good to go out of the box. For reference, it should look like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SpyComponent&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;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;spy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@spy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spy&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_component.html.erb&lt;/code&gt; contains placeholder content provided by the generator right now, so let’s update it to render information about each spy:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;&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;vi&quot;&gt;@spy&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;p&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Name:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@spy&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;/p&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;strong&amp;gt;&lt;/span&gt;Mission:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@spy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mission&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&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;Show this spy&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@spy&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This is just regular old &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;erb&lt;/code&gt;, essentially a copy of the default content of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_spy.html.erb&lt;/code&gt; generated by the Rails scaffold generator we ran during application setup.&lt;/p&gt;

&lt;p&gt;With the spy component in place, we now need to actually use it. To do that, we’ll update the spies index view to use the our new component. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies/index.html.erb&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;notice&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;notice&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;h1&amp;gt;&lt;/span&gt;Spy&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;spies&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;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SpyComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with_collection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@spies&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;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;New spy&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_spy_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re using &lt;a href=&quot;https://viewcomponent.org/guide/collections.html&quot;&gt;collection rendering&lt;/a&gt; to loop through each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@spies&lt;/code&gt; and render each with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_component.html.erb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We’ve now got our list of spies rendering using view components — next up we’ll add Turbo Stream broadcasts so that newly created spies are appended to the list automatically.&lt;/p&gt;

&lt;h2 id=&quot;add-turbo-stream-broadcasts&quot;&gt;Add Turbo Stream broadcasts&lt;/h2&gt;
&lt;p&gt;First, we need to ensure that visitors to the spies index page are subscribed to the appropriate turbo stream channel.&lt;/p&gt;

&lt;p&gt;To do this, we can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream_from&lt;/code&gt; helper from turbo-rails. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies/index.html.erb&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Snip --&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_from&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;spies&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;spies&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;render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SpyComponent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;with_collection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@spies&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;c&quot;&gt;&amp;lt;!-- Snip --&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There are two important pieces here. First, we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream_from&lt;/code&gt; helper, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies&lt;/code&gt; as the name.&lt;/p&gt;

&lt;p&gt;This helper creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-cable-stream-source&amp;gt;&lt;/code&gt; in the rendered HTML with a &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/a0f14b9e4649f60bcf8fa8b82f35e7f460d60177/app/channels/turbo/streams/stream_name.rb&quot;&gt;signed-stream-name&lt;/a&gt; that looks something like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-cable-stream-source&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;channel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Turbo::StreamsChannel&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;signed-stream-name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;aLongSecureName&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/turbo-cable-stream-source&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next, the div wrapping the list of spy components has an id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies&lt;/code&gt;. This id must match the target of the Turbo Stream broadcast, which &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/ba86832f7f13001793ab917185788df9723666e8/app/models/concerns/turbo/broadcastable.rb#L68&quot;&gt;defaults to&lt;/a&gt; the plural name of the model we are broadcasting from. If our wrapper div doesn’t have an id, the broadcast we add next will fail.&lt;/p&gt;

&lt;p&gt;With the stream subscription added to the view, the last step is to add a model callback in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;models/spy.rb&lt;/code&gt; to broadcast newly created spies on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies&lt;/code&gt; channel.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy.rb&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Spy&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;after_create_commit&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:append_new_record&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;append_new_record&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;broadcast_append_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;s1&quot;&gt;&apos;spies&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ApplicationController&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;no&quot;&gt;SpyComponent&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;spy: &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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re using the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;html&lt;/code&gt; option added to &lt;a href=&quot;https://github.com/hotwired/turbo-rails/pull/245&quot;&gt;Turbo Stream broadcast methods&lt;/a&gt; to render the SpyComponent instead of rendering a partial.&lt;/p&gt;

&lt;p&gt;Note that using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController.render&lt;/code&gt; to render a view_component isn’t officially sanctioned by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view_component&lt;/code&gt; team.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;view_component&lt;/code&gt; folks are actively discussing an official way to add support for stream broadcasts, which you can track on &lt;a href=&quot;https://github.com/github/view_component/issues/1106&quot;&gt;this issue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With the model broadcast in place, we’re ready to test our Turbo Stream-enabled view components.&lt;/p&gt;

&lt;p&gt;A note for Rails 7 users: If you’re on Rails 7 alpha2 (the latest release at the time of this writing) an issue exists that will prevent the broadcast from working because of a disabled session error. This issue is entirely unrelated to view components but it will break our broadcast all the same.&lt;/p&gt;

&lt;p&gt;This issue &lt;a href=&quot;https://github.com/rails/rails/pull/43427&quot;&gt;will be fixed&lt;/a&gt; in the next Rails release, but until then you can prevent the issue by updating development.rb with this line:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;action_controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;silence_disabled_session_errors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To see it action, refresh the index page, and then open a new tab to &lt;a href=&quot;http://localhost:3000/spies/new&quot;&gt;http://localhost:3000/spies/new&lt;/a&gt;, create a new spy, and see that the newly created spy is automatically appended to the list of spies automatically.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-11-03-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with two open browser windows. In one, a list of spies is shown. In the other, the user is typing into a form to create a new spy. They finish typing, click a button to create a spy, and the browser window the list of spies is updated to include the newly created spy&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we looked at a technique to render View Components in Turbo Stream broadcasts, leveraging the recently added ability to render html in Turbo Stream broadcasts.&lt;/p&gt;

&lt;p&gt;This article’s focus is on Turbo Stream broadcasts + view_component, so we didn’t dive deep into how Turbo Streams work or how to take full advantage of the real power of view_component.&lt;/p&gt;

&lt;p&gt;To dig deeper, you might find these resources helpful starting points:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The &lt;a href=&quot;https://viewcomponent.org/&quot;&gt;view_component documentation&lt;/a&gt; is excellent, and worth reviewing in detail to understand more about how you can use view components&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.blog/2020-12-15-encapsulating-ruby-on-rails-views/&quot;&gt;Encapsulating Ruby on Rails views&lt;/a&gt; from Github’s blog is a nice introduction to the work behind the ViewComponent library and includes links to see how Github uses ViewComponents in their application&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;Turbo Rails source code&lt;/a&gt;. If you’re serious about using Turbo in your Rails application, thoroughly reviewing the source and accompanying comments is highly recommend.&lt;/li&gt;
  &lt;li&gt;The Turbo &lt;a href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;handbook&lt;/a&gt; is the best place to start if you’re new to Turbo and want to understand the basics of Streams, Frames, and Drive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Hotwiring Rails Newsletter - November 2021</title>
   <link href="https://colby.so/posts/hotwiring-rails-november-2021"/>
   <updated>2021-11-01T00:00:00+00:00</updated>
   <id>https://colby.so/posts/hotwiring-rails-november-2021</id>
   <content type="html">&lt;p&gt;This newsletter went out via email to the Hotwiring Rails subscriber list, but I know that not everyone wants another email cluttering up their inbox. For those folks, I’ll always publish the full content of the newletter here too, so you can get all the content with none of the emails.&lt;/p&gt;

&lt;p&gt;Thoughts or feedback on how I can make this newsletter more valuable? Have something you’d like to include in next month’s edition? Send me an &lt;a href=&quot;mailto:david@colby.so?subject=Hotwiring+Rails&quot;&gt;email&lt;/a&gt;, or find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of contents&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#articles-and-guides&quot;&gt;Articles and guides&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#new-and-interesting-PRs&quot;&gt;New and interesting Rails-land PRs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#recent-pieces-from-my-blog&quot;&gt;Recent pieces from my blog&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-best-thing-i-read-recently&quot;&gt;The best thing I read recently&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;articles-and-guides&quot;&gt;Articles and guides&lt;/h2&gt;
&lt;h3 id=&quot;frontend-bundler-braindump-by-konnor-rogers&quot;&gt;Frontend bundler braindump by Konnor Rogers&lt;/h3&gt;
&lt;p&gt;This deep dive into the world of frontend bundling is a dense, delightful trip down into the weeds. With Rails 7 dropping Webpacker as the default for new apps and the introduction of importmap-rails, cssbundling-rails, and jsbundling-rails, understanding more about bundling will help Rails devs make sense of all of the new options.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/paramagicdev/frontend-bundler-braindump-10fj&quot;&gt;Read it on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;using-hotwire-with-rails-for-a-spa-like-experience-by-mike-wilson&quot;&gt;Using Hotwire with Rails for a SPA like experience by Mike Wilson&lt;/h3&gt;
&lt;p&gt;A neat implementation of list item navigation without a full page turn. Built with Turbo and Stimulus, making great use of a few Turbo events to animate loading states and highlight the active list item. A great read for folks new to the full Hotwire stack&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.mikewilson.dev/posts/using-hotwire-with-rails-for-a-spa-like-experience/&quot;&gt;Read it on mikewilson.dev&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;hotwire-server-rendered-live-previews-by-sean-doyle&quot;&gt;Hotwire: Server-rendered live previews by Sean Doyle&lt;/h3&gt;
&lt;p&gt;Sean’s work is always thoughtful and thorough, and this article is no different. In it, you’ll build a live preview component with Rails, Stimulus, and Turbo that previews the user’s content as they type. As a nice bonus, the implementation gracefully degrades for users without JavaScript enabled.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://thoughtbot.com/blog/hotwire-server-rendered-live-previews&quot;&gt;Read it on thoughtbot.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;hotwire-with-stimulusreflex-by-julian-rubisch&quot;&gt;Hotwire with StimulusReflex by Julian Rubisch&lt;/h3&gt;
&lt;p&gt;Hotwire isn’t the only option out there for building modern Rails applications — CableReady and StimulusReflex are excellent tools to have in your bag. In this slide deck, Julian (a StimulusReflex core team member) talks through how he thinks about combining the Hotwire stack with StimulusReflex. Essential reading if you’re trying to figure out how all of the pieces fit together in the real world.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/julianrubisch/hotwire-with-stimulusreflex-2bh5&quot;&gt;View the slides on speakerdeck.com&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;hotwire-best-practices-for-stimulus-by-pete-hawkins&quot;&gt;Hotwire: best practices for Stimulus by Pete Hawkins&lt;/h3&gt;
&lt;p&gt;This article shows off a few simple, reusable Stimulus controllers that can be dropped in to any Stimulus-enabled application. Autosubmitting, empty states, toggling and more. Simple, generic components that make a great introduction to some useful Stimulus patterns and libraries like stimulus-use for folks getting started with Stimulus.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://dev.to/phawk/hotwire-best-practices-for-stimulus-40e&quot;&gt;Read it on dev.to&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;hotwire-turbo-increment-likes-count-without-any-page-refreshes-by-yaroslav-shmarov&quot;&gt;Hotwire Turbo: Increment likes count without any page refreshes by Yaroslav Shmarov&lt;/h3&gt;
&lt;p&gt;A ﻿short and sweet piece from Yaroslav demonstrating the implementation of a Turbo Stream powered like button. This is a simple introduction for Turbo Stream beginners who are already comfortable with Rails and want to explore what Turbo can add to toolkit.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://blog.corsego.com/hotwire-turbo-button-to-like-record&quot;&gt;Read it on blog.corsego.com&lt;/a&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;new-and-interesting-prs&quot;&gt;New and interesting PRs&lt;/h3&gt;
&lt;h4 id=&quot;rails-7&quot;&gt;Rails 7&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/rails/rails/pull/43427&quot;&gt;Stop failing CSRF token generation when session is disabled&lt;/a&gt; — This PR, merged into Rails main last month, fixes a bug introduced in Rails 7 alpha 1 that breaks rendering forms and buttons outside of the normal controller context. Until this bug fix, rendering something like a delete button from a Turbo Stream or CableReady broadcast was broken in Rails 7, as first described here.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/rails/rails/pull/43507/files&quot;&gt;ActionCable client ensures subscription command is confirmed&lt;/a&gt; — This PR, merged into Rails main last week, should eliminate nagging ActionCable connection failures that were all to common, especially when using Turbo Stream broadcasts.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/rails/rails/pull/41856&quot;&gt;Add support for generated columns in PostgreSQL&lt;/a&gt; — This PR, merged into Rails main in September, adds support for generated columns in Postgres to ActiveRecord, eliminating the need to drop down to writing raw SQL when you need a generated column. Especially useful for things like pg_search migrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;cableready&quot;&gt;CableReady&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/stimulusreflex/cable_ready/pull/145&quot;&gt;Receive morph updates from model/PORO callbacks&lt;/a&gt; — This PR, merged into CableReady’s 5.0 prerelease, adds the ability to broadcast updates from a model to many subscribers, customizing the content for each user. Perfect for SaaS apps with complex permission levels that need to reference current_user in broadcasted content, inspired by Bullet Train’s Sprinkles library.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;cssbundling-rails&quot;&gt;cssbundling-rails&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/rails/tailwindcss-rails/pull/71&quot;&gt;Add Tailwind styled scaffold templates&lt;/a&gt; — This PR, merged into cssbundling-rails in September, does what it says on the tin. Adding some basic, Tailwind-powered scaffolds (with designs sanctioned by the Tailwind team) makes it even easier to get nice looking prototypes together with Rails and Tailwind&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;recent-pieces-from-my-blog&quot;&gt;Recent pieces from my blog&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.colby.so/posts/turbo-frames-on-rails&quot;&gt;Turbo Frames on Rails&lt;/a&gt; — Turbo’s documentation, especially for the turbo-rails gem, is… lacking. While the official docs may eventually catch up, I spent time compiling what I’ve learned over the last year of building with Turbo to write up some simple how-tos, tricks, and gotchas as a companion to my Turbo Streams on Rails article from August.&lt;/li&gt;
  &lt;li&gt;Sorting, searching, and filtering with Hotwire — This is a two part series for folks newer to Turbo that walks through building an (almost) instant search, filter, and sort experience with Turbo Frames and a little bit of Stimulus.
    &lt;ul&gt;
      &lt;li&gt;Part 1: &lt;a href=&quot;https://www.colby.so/posts/sortable-table-with-rails-and-turbo-frames&quot;&gt;Sort tables (almost) instantly with Ruby on Rails and Turbo Frames&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;Part 2: &lt;a href=&quot;https://www.colby.so/posts/filtering-tables-with-rails-and-hotwire&quot;&gt;Filter, search, and sort tables with Rails and Turbo Frames&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-best-thing-i-read-recently&quot;&gt;The best thing I read recently&lt;/h2&gt;
&lt;h3 id=&quot;the-single-page-app-morality-play-by-baldur-bjarnason&quot;&gt;The Single-Page-App Morality Play by Baldur Bjarnason&lt;/h3&gt;
&lt;p&gt;Baldur uses the never ending debate about SPAs as a jumping off point for a broader discussion of the pitfalls that almost every team of web developers deals with as they try (and usually fail) to deliver great software. There’s no code here, and no easy answers, but if you’re reading this newsletter you’ll probably enjoy reading and thinking about Baldur’s piece.&lt;/p&gt;

&lt;blockquote&gt;Until we accept that most of the industry is poorly managed and try to figure out how to solve that problem, sites and web apps won’t get better, just shinier and with more bounce.&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;https://www.baldurbjarnason.com/2021/single-page-app-morality-play&quot;&gt;Read the entire article at baldurbjarnason.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s all for this month, thanks for reading and for your support! If you enjoyed this newsletter, I would appreciate you sharing it with your friends.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Filter, search, and sort tables with Rails and Turbo Frames</title>
   <link href="https://colby.so/posts/filtering-tables-with-rails-and-hotwire"/>
   <updated>2021-10-15T00:00:00+00:00</updated>
   <id>https://colby.so/posts/filtering-tables-with-rails-and-hotwire</id>
   <content type="html">&lt;p&gt;Today we are going to build a table that can be sorted, searched, and filtered all at once, using Ruby on Rails, Turbo Frames, a tiny Stimulus controller, and a little bit of Tailwind for styling.&lt;/p&gt;

&lt;p&gt;We will start with a sortable, Turbo Frame-powered table that displays a list of Players from a database. We built this sortable table in a &lt;a href=&quot;https://www.colby.so/posts/sortable-table-with-rails-and-turbo-frames&quot;&gt;previous article&lt;/a&gt; — you might find it helpful to start with that article, especially if you are new to Turbo.&lt;/p&gt;

&lt;p&gt;When we are finished, users will be able to search for players by name, filter them by their team, and sort the table. Sorting, searching, and filtering all work together, in any combination, and they each occur without a full page turn. The end result will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-10-15-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user interacting with a table on a website. They click column headers to sort the table, use a drop down menu to filter the table by a specific team, and type in a search box to filter the table by name&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This article is intended for folks who are comfortable with Ruby and Rails code. You won’t need any prior experience with Turbo Frames or Stimulus.&lt;/p&gt;

&lt;p&gt;Let’s dive in!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;project-setup&quot;&gt;Project setup&lt;/h2&gt;
&lt;p&gt;If you want to follow along with this article and you haven’t already completed the &lt;a href=&quot;https://www.colby.so/posts/sortable-table-with-rails-and-turbo-frames&quot;&gt;sortable table article&lt;/a&gt; locally, you’ll want to begin by cloning &lt;a href=&quot;https://github.com/DavidColby/player_sorting_frames&quot;&gt;this Github repo&lt;/a&gt;. If you have completed the sortable table article, this one picks up exactly where that one ends, so go ahead and work from where that article finished.&lt;/p&gt;

&lt;p&gt;To set up the application after cloning from Github, from your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;bundle &lt;span class=&quot;nb&quot;&gt;install
&lt;/span&gt;yarn &lt;span class=&quot;nb&quot;&gt;install
&lt;/span&gt;rails db:setup&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Once the application is ready, checkout the &lt;a href=&quot;https://github.com/DavidColby/player_sorting_frames/tree/sortable&quot;&gt;sortable branch&lt;/a&gt;, where this article picks up, and then run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; to compile assets and start your development server.&lt;/p&gt;

&lt;p&gt;After you start the server, head to &lt;a href=&quot;http://localhost:3000&quot;&gt;http://localhost:3000&lt;/a&gt; and see that you have a seeded database of players and that you can sort the table by clicking on each column header.&lt;/p&gt;

&lt;p&gt;If you’re curious how the the sorting works, the sortable tables article goes through the frame-powered sorting mechanism in detail.&lt;/p&gt;

&lt;p&gt;With setup complete, let’s start building!&lt;/p&gt;

&lt;h2 id=&quot;add-a-search-form&quot;&gt;Add a search form&lt;/h2&gt;
&lt;p&gt;We’ll start with a simple search form, added inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; turbo frame in  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/_players.html.erb&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;players&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;shadow overflow-hidden rounded border-b border-gray-200&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;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-end mb-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;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;n&quot;&gt;params&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;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;border border-blue-500 rounded p-2&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search&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;bg-blue-500 text-white p-2 rounded-sm&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;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;c&quot;&gt;&amp;lt;!-- Snip the table --&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This is a standard-issue Rails search form. When the form is submitted, a GET request is dispatched and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController#list&lt;/code&gt; responds to the request. For now, the form requires the user to click the Search button to submit the search request.&lt;/p&gt;

&lt;p&gt;Next, we’ll update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; method in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt; to filter the list of players when the search form is submitted:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players&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;name ilike ?&apos;&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;params&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;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;if&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;:name&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;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;:column&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; &lt;/span&gt;&lt;span class=&quot;si&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;:direction&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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;render&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;s1&quot;&gt;&apos;players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve got a clunky, functional implementation of searching by name — when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; parameter from the form’s text_field is present, we run a case insensitive query for players in the database with a name that matches the search query.&lt;/p&gt;

&lt;p&gt;With these changes in place, refresh the page, type a query into the search form and submit it. You should see the players list update with the results of your query.&lt;/p&gt;

&lt;p&gt;You’ll notice right away that searching clears any previously applied sorting on the table, and sorting the table clears out any search. So we’ve got a search form, but we have to click to submit the form and doing so clears out the user’s sorting preference.&lt;/p&gt;

&lt;p&gt;We’ll fix both of those issues, starting with remove the submit button and searching as the user types instead.&lt;/p&gt;

&lt;h3 id=&quot;real-time-searching&quot;&gt;Real-time searching&lt;/h3&gt;
&lt;p&gt;We’ll use a small Stimulus controller to submit the search form as the user types. First, generate the Stimulus controller with the generator built in to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulus-rails&lt;/code&gt;. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails g stimulus search_form&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then, fill that controller in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/search_form_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;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;nc&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;form&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;nf&quot;&gt;search&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;clearTimeout&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;timeout&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;timeout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTimeout&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;formTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;requestSubmit&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;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;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This controller’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; function calls &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/requestSubmit&quot;&gt;requestSubmit&lt;/a&gt; on a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; target. Since we’ll call this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; function as the user is typing in a text input, we’ve add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clearTimeout&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setTimeout&lt;/code&gt; to ensure that the form isn’t submitted every time the user types a new character.&lt;/p&gt;

&lt;p&gt;Note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit&lt;/code&gt; needs a &lt;a href=&quot;https://github.com/javan/form-request-submit-polyfill&quot;&gt;polyfill&lt;/a&gt; for support on Safari and IE11. As an alternative to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit&lt;/code&gt;, if you are using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rails/ujs&lt;/code&gt; in your application, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rails.fire(this.formTarget, &apos;submit&apos;)&lt;/code&gt; works without a polyfill.&lt;/p&gt;

&lt;p&gt;With the Stimulus controller ready to go, next we’ll connect that controller the search form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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-end mb-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;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;search-form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;search_form_target: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&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;s2&quot;&gt;&quot;players&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;border border-blue-500 rounded p-2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;autocomplete: &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;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;input-&amp;gt;search-form#search&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;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;n&quot;&gt;turbo_frame_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;players&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;shadow overflow-hidden rounded border-b border-gray-200&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;c&quot;&gt;&amp;lt;!-- Snip table --&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we first moved the search form outside of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-frame&lt;/code&gt;. This is necessary because if the search form is inside of the turbo frame, the search input will be reset every time the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; frame is rerendered (meaning every time our search form is submitted), like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-10-15-cleared-search.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing in a search form above a table with a list of players. When they finish typing the table updates based on their query and the search form they were typing is reset&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Because the form is now outside of the frame, we add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-frame&lt;/code&gt; to target the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; frame. This &lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames#targeting-navigation-into-or-out-of-a-frame&quot;&gt;tells Turbo&lt;/a&gt; to use the response from the form submission to replace the content of the players frame.&lt;/p&gt;

&lt;p&gt;We also added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;search-form&quot;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-search_form_target=&quot;form&quot;&lt;/code&gt; to the form element. These Stimulus attributes &lt;a href=&quot;https://stimulus.hotwired.dev/reference/controllers#identifiers&quot;&gt;connect&lt;/a&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search-form&lt;/code&gt; controller to the DOM and set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; &lt;a href=&quot;https://stimulus.hotwired.dev/reference/targets#attributes-and-names&quot;&gt;target&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally, we add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action=“input-&amp;gt;search-form#search”&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; field, which tells Stimulus to call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; function each time the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event&quot;&gt;event&lt;/a&gt; is fired on the name field.&lt;/p&gt;

&lt;p&gt;We moved fairly quickly through some core Stimulus concepts here. The &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus handbook&lt;/a&gt; is a great reference point if you need to spend more time with any of these concepts.&lt;/p&gt;

&lt;p&gt;With these changes in place, we can refresh the page and see that search results update as the user types. We still cannot sort and search at the same time though, so let’s tackle that next.&lt;/p&gt;

&lt;h2 id=&quot;sorting-and-searching-at-the-same-time&quot;&gt;Sorting and searching at the same time&lt;/h2&gt;
&lt;p&gt;The reason we can’t search and sort at the same time is because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; method relies on URL parameters to apply search and filter options. Because the search form doesn’t include the sort parameters and sorting doesn’t include the search parameter, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; has no way of retaining sort and search options across requests — every new request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; starts from scratch.&lt;/p&gt;

&lt;p&gt;There are a variety of ways to address this issue. The most direct is to move from using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; to storing filter options in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our basic approach will be to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; to update a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filters&lt;/code&gt; hash in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt; object. Since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt; is maintained across requests, as long as we update the session &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filters&lt;/code&gt; hash with the search and sort &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; on each request, we can persist search and sort options across requests.&lt;/p&gt;

&lt;p&gt;A very ugly implementation of this concept, done directly in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; method looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&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;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;merge!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players&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;players.name ilike ?&apos;&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;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&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;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&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;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&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; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;direction&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;p&quot;&gt;)&lt;/span&gt;
  
  &lt;span class=&quot;n&quot;&gt;render&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;s1&quot;&gt;&apos;players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&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;filter_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;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;:column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:direction&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we ensure that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;filters&apos;]&lt;/code&gt; is a hash, update its value by merging in whitelisted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_params&lt;/code&gt; and then use the the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filters&lt;/code&gt; hash to search for players by name and order the list of players, as appropriate.&lt;/p&gt;

&lt;p&gt;This ugly code is fully functional — if you update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; method and add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter_params&lt;/code&gt; method to your controller you will be able to search and sort at the same time; however, this code is clunky, difficult to follow, and quickly becomes unmaintainable as your application grows.&lt;/p&gt;

&lt;p&gt;So, if this code is ugly and unmaintainable, why are we looking at it?&lt;/p&gt;

&lt;p&gt;Because we are going to refactor it into something neater and more scalable. Before we do that it is helpful to see what the most direct implementation can be so we can understand what is happening at a basic level.&lt;/p&gt;

&lt;p&gt;When we’re done, we’ll still store &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; in a hash in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt; object, and we’ll still use the hash values to query the database based on the user’s preferences. Our code will be nicer, but it’ll still be the same basic concept.&lt;/p&gt;

&lt;p&gt;Let’s refactor this code next.&lt;/p&gt;

&lt;h2 id=&quot;building-a-filterable-concern&quot;&gt;Building a Filterable concern&lt;/h2&gt;
&lt;p&gt;To make our filtering code more scalable and less error prone, we are going to start with a generalized &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; &lt;a href=&quot;https://api.rubyonrails.org/v6.1.4/classes/ActiveSupport/Concern.html&quot;&gt;concern&lt;/a&gt;. We’ll include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; and use it to filter &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; won’t know anything about the specifics of querying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt;, instead it will just implement logic to store query parameters in the session. Once the values are stored, they’ll be used to apply filters.&lt;/p&gt;

&lt;p&gt;First, create the filterable concern. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/controllers/concerns/filterable.rb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then, fill in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filterable.rb&lt;/code&gt; with the following:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Filterable&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;filter!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;store_filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;apply_filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&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;store_filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;session&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;resource&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;nf&quot;&gt;underscore&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_filters&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;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;key?&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;resource&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;nf&quot;&gt;underscore&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_filters&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;session&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;resource&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;nf&quot;&gt;underscore&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_filters&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;merge!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter_params_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&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;filter_params_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&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;permit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;FILTER_PARAMS&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;apply_filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&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;resource&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;nf&quot;&gt;underscore&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_filters&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There’s a lot of code here, let’s break it down.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter!&lt;/code&gt; method is what we’ll call from the controller to apply filters in response to a request from a user. It takes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource&lt;/code&gt; argument. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource&lt;/code&gt; will be an ActiveRecord class, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Player&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter!&lt;/code&gt; simply calls out to two internal methods, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;store_filters&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apply_filters&lt;/code&gt;, which do the heavy lifting.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;store_filters&lt;/code&gt; ensures that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;class_name_filters&apos;]&lt;/code&gt; exists, and then writes whitelisted parameters into the session key, replicating the first two lines of our ugly implementation directly in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; method in the last section.&lt;/p&gt;

&lt;p&gt;Once the filters are stored, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apply_filters&lt;/code&gt; calls a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; class method from the class we’re interested in which should return a list of ActiveRecord objects.&lt;/p&gt;

&lt;p&gt;You’ll notice here that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; isn’t doing much on its own. Instead, it is relying on methods to exist on the class passed in to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter!&lt;/code&gt;. This is by design — in a real application, we would likely need to build filtering mechanisms for many different ActiveRecord classes, and each will need to be able to filter by a unique set of columns. Trying to build all of that logic into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; module would quickly become even harder to maintain than tossing everything in the controller.&lt;/p&gt;

&lt;p&gt;Rather than building all of that complexity in to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt;, we instead just rely on the target class to define &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FILTER_PARAMS&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; method in whatever way works for that particular class.&lt;/p&gt;

&lt;p&gt;Let’s see this in action by updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/player.rb&lt;/code&gt; to work with our new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; concern.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Player&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;:team&lt;/span&gt;

  &lt;span class=&quot;no&quot;&gt;FILTER_PARAMS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sx&quot;&gt;%i[name column direction]&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;scope&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:by_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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&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;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players.name ilike ?&apos;&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;query&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;p&quot;&gt;}&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;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;by_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&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;nf&quot;&gt;order&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;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&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; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;direction&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve defined the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FILTER_PARAMS&lt;/code&gt; constant with the three filtering we support on the players table.&lt;/p&gt;

&lt;p&gt;Next, we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;by_name&lt;/code&gt; scope to handle searching the players table by name.&lt;/p&gt;

&lt;p&gt;Finally, we implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt;, which replaces the queries that we previously built in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController#list&lt;/code&gt; and makes use of the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;by_name&lt;/code&gt; scope to be a bit more readable.&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Player&lt;/code&gt; set up for filtering, we can update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; to include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; and replace the filtering logic in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; with the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter!&lt;/code&gt; method.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PlayersController&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;Filterable&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# snip&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filter!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Player&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we simply include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; in the controller and then set the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter!&lt;/code&gt; method provided by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Much nicer, right?&lt;/p&gt;

&lt;p&gt;Before moving on, you’ll also notice that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; is in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;controller/concerns&lt;/code&gt; directory, but it isn’t truly a &lt;a href=&quot;https://api.rubyonrails.org/v6.1.4/classes/ActiveSupport/Concern.html&quot;&gt;Concern&lt;/a&gt;. In our case, controller concerns is the simplest place for this module to live, but we don’t need the full functionality of a true concern. You could just as easily place this module in another place if you prefer.&lt;/p&gt;

&lt;p&gt;Our last step is to update the views to read values from the session instead of params so that we always display the correct set of applied filters to users.&lt;/p&gt;

&lt;p&gt;First, update the table header in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_players&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&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;s2&quot;&gt;&quot;name&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;build_order_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &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;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&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;s2&quot;&gt;&quot;teams.name&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;build_order_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&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;s2&quot;&gt;&quot;seasons&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;build_order_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Seasons&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;/th&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We’ve replaced references to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session.dig&lt;/code&gt; calls with this change. When no filters have been applied, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session[&apos;player_filters&apos;]&lt;/code&gt; won’t exist, so we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dig&lt;/code&gt; to avoid nil errors in those cases.&lt;/p&gt;

&lt;p&gt;Next, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/heleprs/players_helper.rb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PlayersHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;build_order_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&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;column&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;column&apos;&lt;/span&gt;&lt;span class=&quot;p&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;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;direction: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;next_direction&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;link_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;direction: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;asc&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;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;next_direction&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;direction&apos;&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;s1&quot;&gt;&apos;asc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;desc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&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;sort_indicator&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;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;sort sort-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;direction&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Again we’re just replacing params with the equivalent session values so that sorting works correctly all the time and visual indicators are shown consistently.&lt;/p&gt;

&lt;p&gt;With those changes in place, refresh the page and see that you can search and sort the table at the same time, and the UI always shows the proper sort indicator.&lt;/p&gt;

&lt;p&gt;Nice work making it this far! We spent a good amount of time building a more scalable filtering solution, so let’s wrap up this article by putting that scalable solution to use by adding a filtering option to the table.&lt;/p&gt;

&lt;h3 id=&quot;add-filtering-by-team&quot;&gt;Add filtering by team&lt;/h3&gt;
&lt;p&gt;Right now users can search by player name and sort the table, but they can’t filter by team or season. Let’s add filtering by team to the filter options.&lt;/p&gt;

&lt;p&gt;We’re going to use a select input for the Team filter, since &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;team&lt;/code&gt; is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;belongs_to&lt;/code&gt; relationship on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Player&lt;/code&gt; class — we’ll have a dropdown menu that displays all available teams by name, each dropdown option will have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;team_id&lt;/code&gt; as the value sent back to the server.&lt;/p&gt;

&lt;p&gt;Since we’re using a select input, let’s make it look nice by taking a slight detour to install Tailwind’s forms plugin:&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;yarn add @tailwindcss/forms&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tailwind.config.js&lt;/code&gt; to include the plugin:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;nf&quot;&gt;require&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;@tailwindcss/forms&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Incredible stuff.&lt;/p&gt;

&lt;p&gt;Back to adding the team filter. We’ll start by adding the team filter to the UI. In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;search-form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;search_form_target: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;form&quot;&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;s2&quot;&gt;&quot;players&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&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;ss&quot;&gt;:team_id&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;no&quot;&gt;Team&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;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;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;team_id&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;span class=&quot;ss&quot;&gt;include_blank: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;All Teams&apos;&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;border-blue-500 rounded&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;change-&amp;gt;search-form#search&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;c&quot;&gt;&amp;lt;!-- Snip the search input --&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This is a regular Rails &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select&lt;/code&gt; helper. In it, we build a list of all teams in the database, set the selected value when one is present (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session.dig&lt;/code&gt;, again) and fire the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; function of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search-form&lt;/code&gt; controller each time the select input changes.&lt;/p&gt;

&lt;p&gt;Refresh the page, change the team input and see that it doesn’t work yet. We haven’t updated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Player&lt;/code&gt; to support filtering by team yet.&lt;/p&gt;

&lt;p&gt;Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/player.rb&lt;/code&gt; and update it like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;FILTER_PARAMS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sx&quot;&gt;%i[name team_id column direction]&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;scope&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:by_team&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;team_id&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;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;team_id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;team_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;n&quot;&gt;team_id&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;p&quot;&gt;}&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;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;by_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;name&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;nf&quot;&gt;by_team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;team_id&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;nf&quot;&gt;order&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;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;column&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; &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;direction&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;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we can start to see the benefit of the work we did in the last section. We can add filtering by a new option with just a few simple changes to the model.&lt;/p&gt;

&lt;p&gt;We updated the model to add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;team_id&lt;/code&gt; to the list of valid &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FILTER_PARAMS&lt;/code&gt;, added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;by_team&lt;/code&gt; scope, and then added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;by_team&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filters&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;No need to change our controller or touch the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; module — updating the model is all we need.&lt;/p&gt;

&lt;p&gt;Refresh the page, apply a team filter and see that filtering by team works along with searching by name and sorting. Great work!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-10-15-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user interacting with a table on a website. They click column headers to sort the table, use a drop down menu to filter the table by a specific team, and type in a search box to filter the table by name&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;filtering-the-index-action&quot;&gt;Filtering the index action&lt;/h2&gt;
&lt;p&gt;We’ll wrap up this exercise by make a few more small adjustments to avoid weirdness when a user visits the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/players&lt;/code&gt; with filtering values already saved in their session.&lt;/p&gt;

&lt;p&gt;First, now that we have access to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter!&lt;/code&gt; method, we can update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action to use that method instead of always setting the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; to all players in the database.&lt;/p&gt;

&lt;p&gt;To do that, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filter!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Player&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With that change in place, we’ll now filter the list of players properly when a user reloads players page after applying filters in a previous request, making the experience on the index page a bit more consistent.&lt;/p&gt;

&lt;p&gt;Finally, since we’re using the session values to restore previously applied filters, we need to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; search field to set its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; from the session. Without this change, when the user applies a name search and then refreshes the page, the list of players will be filtered by name, but the search term won’t be visible in the name field.&lt;/p&gt;

&lt;p&gt;To fix this issue,  update the name field in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Search by name&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;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;player_filters&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;name&apos;&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;border border-blue-500 rounded p-2&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;ss&quot;&gt;autocomplete: &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;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;input-&amp;gt;search-form#search&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; added to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; field, we’ll always show the correct value to users, even on the initial page load.&lt;/p&gt;

&lt;p&gt;With these small changes in places, we’ve now got consistent filtering experience that persists cleanly across requests and can be extended with new options as our user experience requirements change.&lt;/p&gt;

&lt;p&gt;Nice work making it this far!&lt;/p&gt;

&lt;p&gt;You’ve reached the end of the tutorial portion of the article, we’ll finish up by discussing a few ways we could improve this implementation in a production application.&lt;/p&gt;

&lt;h2 id=&quot;production-grade-considerations&quot;&gt;Production-grade considerations&lt;/h2&gt;
&lt;p&gt;While what we built today works fine, there are some things to consider if we were building a real, consumer-facing application.&lt;/p&gt;

&lt;p&gt;Our code works and is pretty easy to maintain, but before we go, let’s touch on a few points to think about for production-grade applications. These are intended simply as things to think about as you build, and we won’t be going through any code here:&lt;/p&gt;

&lt;h3 id=&quot;session-storage-limitations&quot;&gt;Session storage limitations&lt;/h3&gt;
&lt;p&gt;We are relying on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt; object to store filter options. This is fine for small applications, but as you grow, you may run into the limits of storing options like this in the session.&lt;/p&gt;

&lt;p&gt;By default, Rails stores the session in a cookie which can only be ~4kb before it will start raising errors. You can use a &lt;a href=&quot;https://guides.rubyonrails.org/v4.1.4/action_controller_overview.html#session&quot;&gt;different session store&lt;/a&gt; but you may want to consider a more flexible solution for storing filter options.&lt;/p&gt;

&lt;p&gt;One option here is to use &lt;a href=&quot;https://github.com/rails/kredis&quot;&gt;Kredis&lt;/a&gt;, a Redis-based solution that provides a nice interface for solving problems like our session persistance problem today.&lt;/p&gt;

&lt;h3 id=&quot;replace-helper-methods&quot;&gt;Replace helper methods&lt;/h3&gt;
&lt;p&gt;The helper methods we are using to render sorting links and indicators could be implemented as &lt;a href=&quot;https://viewcomponent.org/&quot;&gt;ViewComponents&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This pattern allows us to write more testable and reusable view code and excels in cases like our example application. As this application grows, we should expect to have multiple tables across our application that all need sort links.&lt;/p&gt;

&lt;p&gt;Rather than implementing them as helpers that are very specific to the players table, we could create them as generic components that can be thoroughly tested and then reused throughout the code base.&lt;/p&gt;

&lt;h3 id=&quot;expand-filterable-implementation&quot;&gt;Expand Filterable implementation&lt;/h3&gt;
&lt;p&gt;Right now, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; relies on each model to define &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FILTER_PARAMS&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; method from scratch.&lt;/p&gt;

&lt;p&gt;In the real world, there would likely be enough overlap between the different implementations of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; in each model that we would benefit from moving &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filter&lt;/code&gt; out of each model and into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filter&lt;/code&gt; class that defines some shared logic, like applying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt;, which is likely to be the same for every class.&lt;/p&gt;

&lt;p&gt;For a really detailed implementation of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Filterable&lt;/code&gt; pattern, take a look at the &lt;a href=&quot;https://www.stimulusreflexpatterns.com/patterns/filterable_reflex/&quot;&gt;filterable_reflex&lt;/a&gt; from StimulusReflex Patterns.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we built on a Turbo Frame foundation to expand a sortable table view to a table that can be searched, filtered, and sorted without full page turns or lots of custom JavaScript.&lt;/p&gt;

&lt;p&gt;Because frames are so powerful, we were able to easily hook into our existing frame code and add in searching and filtering without thinking too much about the front end implementation. It mostly just worked, once we built the filtering logic on the server.&lt;/p&gt;

&lt;p&gt;This is the power of Turbo Frames — we can build fast, efficient user interfaces without stepping much outside of standard Rails code. The client side code stays light and maintainable, while our server looks and feels familiar to any level of Rails developer.&lt;/p&gt;

&lt;p&gt;To dig deeper into Turbo and building modern Ruby on Rails applications with the Hotwire stack:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Read my articles on &lt;a href=&quot;https://www.colby.so/posts/turbo-frames-on-rails&quot;&gt;Turbo Frames&lt;/a&gt; and &lt;a href=&quot;https://www.colby.so/posts/turbo-streams-on-rails&quot;&gt;Turbo Streams&lt;/a&gt; on Rails&lt;/li&gt;
  &lt;li&gt;Dive into the &lt;a href=&quot;https://github.com/hotwired/turbo&quot;&gt;Turbo&lt;/a&gt; and &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;turbo-rails&lt;/a&gt; source and follow along with the Github activity for both&lt;/li&gt;
  &lt;li&gt;Join the &lt;a href=&quot;https://discuss.hotwired.dev/&quot;&gt;hotwire discussion forums&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Turbo Frames on Rails</title>
   <link href="https://colby.so/posts/turbo-frames-on-rails"/>
   <updated>2021-10-08T00:00:00+00:00</updated>
   <id>https://colby.so/posts/turbo-frames-on-rails</id>
   <content type="html">&lt;p&gt;Turbo, part of the Hotwire tripartite, gives you the tools to write dramatically less custom JavaScript than you would otherwise need to build modern, performant web applications.&lt;/p&gt;

&lt;p&gt;Turbo is composed of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Drive&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Frames&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Streams&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Native&lt;/code&gt;. Each is a valuable piece of the puzzle but today we’re going to focus on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Frames&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Turbo Frames “&lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames&quot;&gt;allow predefined parts of a page to be updated on request&lt;/a&gt;.” Used wisely, frames allow developers to decompose their UI into independently updated pieces, quickly.&lt;/p&gt;

&lt;h2 id=&quot;why-should-i-use-turbo-frames&quot;&gt;Why should I use Turbo Frames?&lt;/h2&gt;
&lt;p&gt;Turbo Frames unlock a huge amount of potential with minimal changes to existing code and they can be gradually introduced to existing projects without necessitating major architectural changes.&lt;/p&gt;

&lt;p&gt;In greenfield projects designed with Turbo Frames in mind, a small team of developers can use frames to deliver fast, efficient user interfaces in dramatically less time than would be required when build a SPA-powered front end.&lt;/p&gt;

&lt;p&gt;While Turbo Frames can be used with many different tech stacks, Rails developers will find the tight integration of frames into Rails (via the &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;turbo-rails&lt;/a&gt; gem) makes using frames in Rails a breeze.&lt;/p&gt;

&lt;h2 id=&quot;the-turbo-documentation-gap&quot;&gt;The Turbo documentation gap&lt;/h2&gt;
&lt;p&gt;As with many new technologies, one of the barriers to getting started with Turbo Frames is inconsistent/incomplete documentation. Along with problems in the official documentation, much of the tutorial content written about Turbo Frames is already out of date.&lt;/p&gt;

&lt;p&gt;Turbo has evolved quickly since its release in December of 2020, and the documentation and supporting tutorials from content creators have struggled to keep pace.&lt;/p&gt;

&lt;p&gt;As an example of the documentation gap, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; doesn’t have any dedicated usage documentation, instead just linking to the base Turbo docs and letting users figure out the rest.&lt;/p&gt;

&lt;p&gt;This means that learning to use Turbo Frames in your Rails application today often requires reading the docs, then digging through source code, and then Googling your way through Github issues and forum posts. Turbo Frames can be difficult to approach.&lt;/p&gt;

&lt;p&gt;In time the documentation will improve and the community will coalesce around best practices and standards that can be more easily communicated to new users.&lt;/p&gt;

&lt;p&gt;In the meantime, here we are.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-is-this-article-who-is-it-for&quot;&gt;What is this article? Who is it for?&lt;/h2&gt;
&lt;p&gt;Today, I’m going to share what I’ve learned over nearly a year building applications with Turbo Frames, from small hobby projects and experiments to multiple large, legacy production applications.&lt;/p&gt;

&lt;p&gt;By doing so, I’m hoping to address the gaps in documentation, to save you from spending too much time reading source code, trawling forum posts and Github issues.&lt;/p&gt;

&lt;p&gt;I will cover content you can find in the documentation (if you know where to look), gotchas and tricks learned from real world experience and picked up from reading way too much about Turbo and Turbo Frames since Turbo’s initial release. I’ll also share patterns that illuminate some of the common web application needs that frames are perfect for.&lt;/p&gt;

&lt;p&gt;This content is geared towards Rails developers interested in using Turbo. The code samples and concepts will be illustrated using Ruby and Rails code and conventions. If you aren’t working in Rails, you should still find plenty of value here, but know that you may not have access to convenience methods like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;%= turbo_frame_tag %&amp;gt;&lt;/code&gt; in your chosen language and framework.&lt;/p&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;

&lt;h2 id=&quot;what-are-turbo-frames&quot;&gt;What are Turbo Frames?&lt;/h2&gt;
&lt;p&gt;At a high level, &lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames&quot;&gt;Turbo Frames&lt;/a&gt; are pieces of a webpage that can be updated independently, without impacting the rest of the content on the page.&lt;/p&gt;

&lt;p&gt;Links and forms within a frame will, by default, attempt to update only the content of the containing frame, whether the server sends a completely new HTML document or only a page fragment.&lt;/p&gt;

&lt;p&gt;Turbo Frames allow developers to decompose a page into pieces of content that can be updated individually as new information is received from a server.&lt;/p&gt;

&lt;p&gt;In practice, common use cases for Turbo Frames include:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Tabbed content&lt;/li&gt;
  &lt;li&gt;In-line editing&lt;/li&gt;
  &lt;li&gt;Searching, sorting, and filtering data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s start looking at code.&lt;/p&gt;

&lt;h2 id=&quot;constructing-a-frame&quot;&gt;Constructing a frame&lt;/h2&gt;
&lt;p&gt;A basic Turbo Frame, rendered with the &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb&quot;&gt;built-in helper&lt;/a&gt; from Turbo Rails (and using erb), looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;some_id&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;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    Some framed 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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The only required argument for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; is an id.&lt;/p&gt;

&lt;p&gt;When the helper is processed, the final HTML is:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;some_id&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;
    Some framed 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;/turbo-frame&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Instead of providing a static id, we can also pass in an active record object, which the &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb#L27&quot;&gt;helper will use&lt;/a&gt; to generate a unique id via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dom_id(object.id)&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;no&quot;&gt;Comment&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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Frames can also receive an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute. When &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; is supplied, the frame will be populated after the initial page load via a separate HTTP request to the frame’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;comments&quot;&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;comments_path&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&amp;gt;&lt;/span&gt;
    Placeholder 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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, the page will initially load the turbo frame with the placeholder div and then immediately a request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;comments_path&lt;/code&gt; endpoint is made and the content of the Turbo Frame is replaced with the response from the server, provided our server returns HTML with a matching Turbo Frame tag.&lt;/p&gt;

&lt;p&gt;What’s a matching Turbo Frame tag? Let’s look at that next.&lt;/p&gt;

&lt;h2 id=&quot;responding-with-frame-content&quot;&gt;Responding with frame content&lt;/h2&gt;
&lt;p&gt;The power of Turbo Frames is in replacing pieces of the page with content sent from the server — without touching the rest of the DOM.&lt;/p&gt;

&lt;p&gt;Updating an existing frame just requires responding to a request with HTML that contains a Turbo Frame element with an id that matches the id of the frame target sent in the request.&lt;/p&gt;

&lt;p&gt;Taking the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;comments&lt;/code&gt; example from above, when a request is made to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/comments&lt;/code&gt;, the server should respond with HTML that contains a frame like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;comments&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;!-- A list of comments, perhaps --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The content inside of the matched frame is replaced with the updated content from the server, without touching the rest of the page.&lt;/p&gt;

&lt;h2 id=&quot;navigating-within-frames&quot;&gt;Navigating within frames&lt;/h2&gt;
&lt;p&gt;By default, links and forms within a Turbo Frame will perform the navigation within the frame, rather than performing a full page turn.&lt;/p&gt;

&lt;p&gt;For a simple example, we can imagine that we have a list of comments on a page and that each comment is wrapped in a Turbo Frame tag, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@comments&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;comment&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;turbo_frame_tag&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;comment&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&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;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;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;n&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This ERB results in rendered HTML:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;comment_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&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;/comments/1/edit&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comment 1&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&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;/turbo-frame&amp;gt;&lt;/span&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;comment_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&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;/comments/2/edit&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comment 2&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&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;/turbo-frame&amp;gt;&lt;/span&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;comment_3&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/comments/3/edit&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comment 3&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&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;/turbo-frame&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With this Turbo-powered HTML in place, clicks on the edit links will make a Turbo Frame request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/comments/:id/edit&lt;/code&gt;, retrieve some HTML, and return that HTML to the browser.&lt;/p&gt;

&lt;p&gt;The Turbo Frame request is a normal HTML request with an additional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo-Frame&lt;/code&gt; header included in the request, with a value that matches the id of the target Turbo Frame.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt;, the presence of this header can be used to &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb#L21&quot;&gt;identify a Turbo Frame request&lt;/a&gt; and respond with appropriate content, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# This approach gets messy in controller actions. Read further on for a variant based approach that scales much more cleanly&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_request?&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;some_turbo_frame_partial&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;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;some_other_partial&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; responds to a Turbo Frame request, it &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb#L16&quot;&gt;automatically removes&lt;/a&gt; the layout from the HTML response. Since Turbo will discard all of the HTML but the requested frame, Rails skips rendering content that it knows won’t be used.&lt;/p&gt;

&lt;p&gt;When a response to a Turbo Frame request is received, &lt;a href=&quot;https://turbo.hotwired.dev/reference/drive&quot;&gt;Turbo Drive&lt;/a&gt; replaces the content of the target frame, leaving the rest of the page untouched.&lt;/p&gt;

&lt;p&gt;Continuing with the above comments example, the edit view may look something like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;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;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;inline-field&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&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;/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;actions&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;After clicking to edit a comment then, the updated HTML after Turbo processes the response would look something like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Form content rendered from /edit --&amp;gt;&lt;/span&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;comments_1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://localhost:3000/comments/1/edit&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;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/comments/1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;accept-charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;UTF-8&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;method=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;post&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;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;_method&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;patch&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;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;authenticity_token&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;some_token&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;inline-field&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;label&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;for=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comment_body&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Body&lt;span class=&quot;nt&quot;&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Comment 1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comment[body]&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comment_body&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;actions&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;input&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;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;commit&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;value=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Update Comment&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;/form&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- The other frames, untouched by the /edit request --&amp;gt;&lt;/span&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;comment_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&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;/comments/2/edit&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comment 2&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&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;/turbo-frame&amp;gt;&lt;/span&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;comment_3&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/comments/3/edit&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comment 3&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&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;/turbo-frame&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finishing up this example, what happens when the user submits the comment form?&lt;/p&gt;

&lt;p&gt;After submit in a normal Rails action, we might redirect the user to the comment show page. When the request occurs within a Turbo Frame, a “redirect” still occurs, but rather than redirecting the page in the browser, the redirect is followed, the new HTML content is sent from the server, and that HTML is used to update the frame.&lt;/p&gt;

&lt;p&gt;In our example, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;comment#update&lt;/code&gt; controller action might look like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;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;k&quot;&gt;if&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;update&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;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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redirect_to&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;else&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;p&quot;&gt;{&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And the show view:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;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&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;@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;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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This behavior allows us to build Turbo Frame-powered interfaces while making very few or no changes to a standard Rails controller, making it possible to prototype in Rails very quickly.&lt;/p&gt;

&lt;p&gt;With just what we’ve learned so far, we’re already in position to start building simple interfaces with Turbo Frames that allow us to add significant amounts of interactivity to our application with very few changes to the Ruby and HTML we’d have in any Rails app.&lt;/p&gt;

&lt;p&gt;Let’s keep exploring, there’s plenty more to see.&lt;/p&gt;

&lt;h3 id=&quot;breaking-out-of-a-frame&quot;&gt;Breaking out of a frame&lt;/h3&gt;
&lt;p&gt;While navigating within a frame is very handy, sometimes, a form or link within a frame needs to break out of the frame and perform a normal page turn.&lt;/p&gt;

&lt;p&gt;To add that behavior to a navigation element within a frame, simply add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-frame=&quot;_top&quot;&lt;/code&gt; to the element, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;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;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;n&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;s2&quot;&gt;&quot;_top&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h3 id=&quot;targeting-a-frame-from-the-outside&quot;&gt;Targeting a frame from the outside&lt;/h3&gt;
&lt;p&gt;Eventually, you’ll find yourself needing to update the content of a frame using a link that isn’t wrapped in your target frame. This often comes up when building tabbed content; the navigation menu will be placed outside of the Turbo Frame that contains the actual content.&lt;/p&gt;

&lt;p&gt;To handle situations like this, we can use the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-frame&lt;/code&gt; data attribute to tell Turbo that the navigation should occur in a specific turbo frame.&lt;/p&gt;

&lt;p&gt;Using our tabbed content example, we may have a page layout like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&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;user/1/profile&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-turbo-frame=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;main&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      Profile
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;li&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;user/1/favorites&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-turbo-frame=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;main&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      Favorites
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&amp;gt;&lt;/span&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;main&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Clicks on either of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user&lt;/code&gt; links will generate a request with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo-Frame: main&lt;/code&gt; header and the response will update the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; turbo frame, even though the links are not wrapped within the frame.&lt;/p&gt;

&lt;h2 id=&quot;lazy-loading-frames&quot;&gt;Lazy loading Frames&lt;/h2&gt;
&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute on a frame can be combined with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loading=lazy&lt;/code&gt; to mark a frame as lazy-loaded. Lazy-loaded frames will not fetch their content from the server until they’re visible on the page.&lt;/p&gt;

&lt;p&gt;Lazy loading is especially helpful for modals, popovers, and other non-critical, below-the-fold content.&lt;/p&gt;

&lt;p&gt;Lazy loaded frames can be combined with a spinner or other loading indicator, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;lazy_frame&quot;&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;comments_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;loading: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;lazy&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;nt&quot;&gt;&amp;lt;div&amp;gt;&lt;/span&gt;I&apos;m a loading spinner&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loading=&quot;lazy&quot;&lt;/code&gt; must be combined with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute, otherwise lazy loading does nothing&lt;/p&gt;

&lt;h2 id=&quot;frame-events&quot;&gt;Frame events&lt;/h2&gt;
&lt;p&gt;Turbo comes packed with &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;lifecycle events&lt;/a&gt;, including two frame specific events:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;turbo:frame-render&lt;/li&gt;
  &lt;li&gt;turbo:frame-load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events (added in &lt;a href=&quot;https://github.com/hotwired/turbo/releases/tag/v7.0.0-rc.2&quot;&gt;Turbo 7.0.0-rc2&lt;/a&gt;) fire each time a frame is loaded or reloaded. Developers can use these events to attach behavior to an element within a frame or animate the entry of a frame element, and they pair nicely with &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In addition to these built in events, a useful complement to the emitted events is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;busy&lt;/code&gt; attribute. The &lt;a href=&quot;https://github.com/hotwired/turbo/blob/main/src/core/frames/frame_controller.ts#L172&quot;&gt;busy attribute&lt;/a&gt; is automatically applied to a frame when a request for the frame’s content begins and is removed when the request finishes.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// From Turbo&apos;s source code: turbo/src/util.ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;markAsBusy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;elements&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;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;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;elements&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;localName&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;turbo-frame&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;busy&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;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;nf&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;aria-busy&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This attribute can be useful for adding loading indicators to long running requests and otherwise tracking the state of frames between the beginning of a frame request and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-render&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:frame-load&lt;/code&gt; events firing.&lt;/p&gt;

&lt;h2 id=&quot;refreshing-frames&quot;&gt;Refreshing Frames&lt;/h2&gt;
&lt;p&gt;Sometimes, users need the ability to refresh the contents of a frame without refreshing the entire page.&lt;/p&gt;

&lt;p&gt;In ideal circumstances, a &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb&quot;&gt;Turbo Stream broadcast&lt;/a&gt; automatically updates the contents of the frame without needing a manual refresh, we don’t always get to work in ideal circumstances.&lt;/p&gt;

&lt;p&gt;In earlier versions of Turbo, refreshing a frame required &lt;a href=&quot;https://github.com/hotwired/turbo/issues/269&quot;&gt;workarounds&lt;/a&gt; like appending a timestamp to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute. Today, those workarounds are no longer necessary and Turbo will happily update a frame any number of times with an indentical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; on each request.&lt;/p&gt;

&lt;p&gt;We can build a refreshable frame like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;%# app/views/games/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;vi&quot;&gt;@game&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&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_score&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_score&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;Update score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;game_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@game&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, the first time a user clicks the update link, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-frame&lt;/code&gt; will be updated with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;game_10&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;reloadable=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://localhost:3000/games/10&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Additional clicks on the update link will fetch updated scores from the server and reload the content of the frame.&lt;/p&gt;

&lt;p&gt;Note that a &lt;a href=&quot;https://github.com/hotwired/turbo/pull/487&quot;&gt;currently open PR&lt;/a&gt; (as of the time this article was last updated, December 2021) proposes an alternative implementation of reloadable frames that will prevent the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reloadable&lt;/code&gt; attribute from being exposed while keeping the behavior intact. In future versions of Turbo the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reloadable&lt;/code&gt; attribute may not longer exist or be neccessary.&lt;/p&gt;

&lt;h2 id=&quot;conditional-template-rendering-using-variants&quot;&gt;Conditional template rendering using variants&lt;/h2&gt;
&lt;p&gt;As you use Turbo Frames in Rails, you’ll eventually run into the desire to have a single controller action respond to both Turbo Frame requests and regular, full-page requests.&lt;/p&gt;

&lt;p&gt;A common example of this is rendering a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/new&lt;/code&gt; page as both a standalone page and as frame content inside of a modal. You could implement a conditional in your controller action to check for the Turbo Frame header, but that quickly starts to clutter up your controllers.&lt;/p&gt;

&lt;p&gt;An alternative approach is to use &lt;a href=&quot;https://guides.rubyonrails.org/layouts_and_rendering.html#the-variants-option&quot;&gt;variants&lt;/a&gt; to render different content based on the inbound request headers, to keep your controllers cleaner.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:turbo_frame_request_variant&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;turbo_frame_request_variant&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;variant&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:turbo_frame&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_request?&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This approach adds a before_action to the ApplicationController that checks if the inbound request is a Turbo Frame request using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_request?&lt;/code&gt; method &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb#L21&quot;&gt;provided by turbo-rails&lt;/a&gt;. When the request is a turbo frame, Rails will look for a Turbo Frame variant (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new.html+turbo_frame.erb&lt;/code&gt;, for example) and render that variant when it exists. Otherwise, the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;html.erb&lt;/code&gt; view will be used&lt;/p&gt;

&lt;p&gt;This approach was originally described &lt;a href=&quot;https://github.com/hotwired/turbo/issues/378#issuecomment-914615544&quot;&gt;here&lt;/a&gt;. While there was a &lt;a href=&quot;https://github.com/hotwired/turbo-rails/pull/250&quot;&gt;proposal&lt;/a&gt; to add this variant-based approach to turbo-rails, those proposals were rejected. The guidance from the maintainers is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_request?&lt;/code&gt; to detect Turbo Frame requests is the only support that will be added because &lt;a href=&quot;https://github.com/hotwired/turbo-rails/pull/250#issuecomment-942079485&quot;&gt;“If you’re branching all your responses for frames vs not, something isn’t right, and we should investigate those pressures in a different way.”&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In my experience, judicious use of variants to serve different content for Frame requests is sometimes the simplest and cleanest path to achieve a desired outcome but YMMV.&lt;/p&gt;

&lt;h2 id=&quot;limitations--gotchas&quot;&gt;Limitations &amp;amp; Gotchas&lt;/h2&gt;
&lt;h3 id=&quot;turbo-frames-and-page-layouts&quot;&gt;Turbo Frames and page layouts&lt;/h3&gt;
&lt;p&gt;Turbo Frames in the DOM are implemented as custom HTML elements (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; tag we’ve seen in this article). We can add classes to them (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;%= turbo_frame_tag post, class: &quot;classes&quot; %&amp;gt;&lt;/code&gt;) and style them; however, because they exist in the DOM and need to wrap their content to be useful, inserting them into existing layouts can cause issues without some planning.&lt;/p&gt;

&lt;p&gt;In particular, you can’t place a frame inside of a table row. While there are &lt;a href=&quot;https://github.com/hotwired/turbo/issues/48#issuecomment-882575388&quot;&gt;workarounds&lt;/a&gt; for this, it is worth knowing that tables and Turbo Frames don’t play well together out of the box.&lt;/p&gt;

&lt;p&gt;In addition to table issues, you may sometimes run into layout issues with flex box and grid layouts when frames are the direct descendant of the container element. You’ll sometimes find that your nicely spaced three element flex layout now breaks because all three elements need to be wrapped in a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;A route worth exploring in these situations is adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;display: contents&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; element, as mentioned &lt;a href=&quot;https://github.com/hotwired/turbo/issues/48#issuecomment-925760725&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;responding-to-a-turbo-frame-request-without-a-matching-turbo-frame&quot;&gt;Responding to a Turbo Frame request without a matching Turbo Frame&lt;/h3&gt;
&lt;p&gt;As you build applications with Turbo Frames, you will inevitably run into an issue in development where you click a link within a frame and all of the content within the frame disappears. You’ll check the Rails logs and see the view you expected was rendered without errors.&lt;/p&gt;

&lt;p&gt;Why did everything disappear?&lt;/p&gt;

&lt;p&gt;The first thing to do when this happens is to check the JavaScript console for errors. Turbo’s JavaScript expects that the HTML response to a Turbo Frame request will contain a matching Turbo Frame element. If no match is found, Turbo will empty the frame of all content and raise an error like this in the JavaScript console:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;Response has no matching &amp;lt;turbo-frame &lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;comments&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; element&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This can be a little confusing at first, since the content renders fine from the server but once you get used to Turbo processing the rendered HTML and updating the DOM, you’ll be able to quickly debug missing frame element errors like this one.&lt;/p&gt;

&lt;p&gt;When in doubt, check for JavaScript errors.&lt;/p&gt;

&lt;h3 id=&quot;updating-page-url-when-navigating-within-frame&quot;&gt;Updating page URL when navigating within frame&lt;/h3&gt;
&lt;p&gt;When a user clicks on a link within a frame, the URL of the page won’t change. This makes sense most of the time — the user hasn’t navigated to a new page, they’ve simply updated some of the content on the existing page.&lt;/p&gt;

&lt;p&gt;One use case where the URL not changing can be problematic is when the user is applying sorting and filtering options by clicking links within a frame. Imagine a table layout with links to sort the table by each column, for example.&lt;/p&gt;

&lt;p&gt;In those cases, a developer may wish to update the URL of the page to reflect the currently applied filters so that the user can copy/paste the URL to share a specific view of the table.&lt;/p&gt;

&lt;p&gt;As of &lt;a href=&quot;https://github.com/hotwired/turbo/releases/tag/v7.1.0&quot;&gt;Turbo’s 7.1 release&lt;/a&gt;, developers can now update the page URL from a frame navigation by adding a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-action&lt;/code&gt; data attribute to the form or link that triggers the frame navigation. Options for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-action&lt;/code&gt; are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt;, to replace the current URL in the browser’s history, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt;, to push a new entry in the history. When building search forms, you’ll likely want to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt; to update the page URL with each new search. In Rails, using a standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form_with&lt;/code&gt; search form, your code to push frame navigation into the browser’s history might look like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;customers_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;s2&quot;&gt;&quot;customers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;advance&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;c&quot;&gt;&amp;lt;%# Some form content goes here %&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we looked in detail at the current state of Turbo Frames — what they are, how to use them in Rails, and the some of the ways frames can help you build modern, efficient applications.&lt;/p&gt;

&lt;p&gt;Turbo Frames in Rails applications naturally pair very well with Stimulus and work exceptionally well when paired with Turbo Streams to manage broadcasted updates and more targeted content updates.&lt;/p&gt;

&lt;p&gt;With Hotwire coming &lt;a href=&quot;https://world.hey.com/dhh/rails-7-will-have-three-great-answers-to-javascript-in-2021-8d68191b&quot;&gt;by default&lt;/a&gt; to new Rails 7 applications, I’m happy to report that after a year of use, building Ruby on Rails applications with the Hotwire stack is simple, straightforward, and adds very little overhead to your development experience. For many Rails applications, all of the reactivity you need can be achieved with the Hotwire stack, and Turbo Frames will play a key role in many applications in the years to come.&lt;/p&gt;

&lt;p&gt;While you begin to experiment, keep in mind that Turbo is still in very active development. Throughout this article, I linked to open PRs that will improve Turbo Frames and make some of the techniques described in this article simpler to implement — as time goes on, expect Turbo and Turbo Frames to continue to improve and for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; to continue to add functionality to help Rails developers use frames effectively in their applications.&lt;/p&gt;

&lt;p&gt;Ready to go deeper? (Some shameless self-promotion ahead, sorry!)&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;I wrote about &lt;a href=&quot;https://www.colby.so/posts/handling-modal-forms-with-rails-and-hotwire&quot;&gt;using the Hotwire stack&lt;/a&gt; to build server rendered modal forms in Rails&lt;/li&gt;
  &lt;li&gt;Build a &lt;a href=&quot;https://thoughtbot.com/blog/hotwire-typeahead-searching&quot;&gt;typeahead search interface&lt;/a&gt; with Turbo Frames&lt;/li&gt;
  &lt;li&gt;Use a Turbo Stream + Turbo Frame combo to &lt;a href=&quot;https://www.colby.so/posts/conditional-rendering-with-turbo-stream-broadcasts&quot;&gt;render different content&lt;/a&gt; to different users from a single stream broadcast&lt;/li&gt;
  &lt;li&gt;Read my &lt;a href=&quot;https://www.colby.so/posts/turbo-streams-on-rails&quot;&gt;exploration&lt;/a&gt; of Turbo Streams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, if you want to learn about implementing Turbo Frames in a complex Rails application, I wrote &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats&quot;&gt;a book&lt;/a&gt; on building a modern Ruby on Rails application with my preferred stack, including Hotwire, StimulusReflex, CableReady, and friends. The book is written in this same, step-by-step tutorial style that you find throughout this blog and aims to offer solutions for a variety of common, real-world web application problems without reaching for the weight of a full frontend framework.&lt;/p&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Sort tables (almost) instantly with Ruby on Rails and Turbo Frames</title>
   <link href="https://colby.so/posts/sortable-table-with-rails-and-turbo-frames"/>
   <updated>2021-09-19T00:00:00+00:00</updated>
   <id>https://colby.so/posts/sortable-table-with-rails-and-turbo-frames</id>
   <content type="html">&lt;aside style=&quot;padding: 1rem; border-radius: .375; background-color: #EFF6FF;&quot;&gt;
  &lt;p style=&quot;color: #1D4ED8; line-height: 1.25rem; font-size: 0.875rem;&quot;&gt;An updated version of this article for Rails 7 and Turbo 8 was published in March of 2024. Everything in this article still works, but the updated article presents a simpler path with the new tools added in Turbo 8. Read the &lt;a href=&quot;https://www.colby.so/posts/turbo-8-refresh-sorting&quot;&gt;updated article here&lt;/a&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;One of the wonderful things about working in Rails in 2021 is that we are spoiled for options when it comes to building modern, reactive applications. When we need to build highly interactive user experiences, we don’t need to reach for JavaScript frameworks like Vue or React — the Rails ecosystem has all the tools we need to deliver exceptionally fast, efficient, and easy-to-maintain front ends.&lt;/p&gt;

&lt;p&gt;Yesterday, I &lt;a href=&quot;https://www.colby.so/posts/a-sortable-table-with-rails-and-stimulusreflex&quot;&gt;published an article&lt;/a&gt; demonstrating a simple implementation of a sortable table with StimulusReflex. Today, we’re going to build the same experience with &lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames&quot;&gt;Turbo Frames&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;Why build the same thing with two different tools? Because we have great options to choose from in Rails-land, and understanding each option is a great place to start when considering which tool is right for you and your team.&lt;/p&gt;

&lt;p&gt;Like yesterday, our application is going to allow users to view a table of players. They’ll be able to click on each header cell to sort the table in ascending and descending order.&lt;/p&gt;

&lt;p&gt;Sorting will happen very quickly, without a full-page turn, and we won’t be writing any custom JavaScript or doing anything outside of writing ordinary Ruby code and ERB templates.&lt;/p&gt;

&lt;p&gt;When we’re finished, the application will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-20-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on column headers on a data table. With each click, the table sorts itself by that column and a triangle indicator appears next to the column used for sorting&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You can demo the application for yourself &lt;a href=&quot;https://aqueous-stream-31641.herokuapp.com/&quot;&gt;on Heroku&lt;/a&gt; (the free dyno might need a moment to wake up when you visit it) or view the &lt;a href=&quot;https://github.com/DavidColby/player_sorting_frames/tree/sortable&quot;&gt;complete source&lt;/a&gt; on Github.&lt;/p&gt;

&lt;p&gt;This article assumes that you are comfortable building applications with Ruby on Rails and may be difficult to follow if you have never worked with Rails before. Previous experience with Turbo Frames is not required.&lt;/p&gt;

&lt;p&gt;Let’s get started!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;To begin, we’re going to create a new Rails application, skipping webpacker in favor of the newly released &lt;a href=&quot;https://github.com/rails/cssbundling-rails&quot;&gt;css&lt;/a&gt; and &lt;a href=&quot;https://github.com/rails/jsbundling-rails&quot;&gt;jsbundling&lt;/a&gt; gems.&lt;/p&gt;

&lt;p&gt;If you prefer skipping the setup steps, you can clone down the example repo &lt;a href=&quot;https://github.com/DavidColby/player_sorting_frames/tree/main&quot;&gt;from Github&lt;/a&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; branch is pinned to the end of the setup process and ready for you to start building.&lt;/p&gt;

&lt;p&gt;This article is being written using Rails 6.1. When Rails 7 releases, the install command listed below will change. Until then we need to manually add the bundling gems to our Gemfile.&lt;/p&gt;

&lt;p&gt;First, from your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new player_sort_frames_1 &lt;span class=&quot;nt&quot;&gt;--skip-javascript&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--skip-webpack-install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--skip-turbolinks&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;player_sort_frames_1
bundle add cssbundling-rails jsbundling-rails hotwire-rails faker
rails g model Team name:string                                                  
rails g scaffold Player name:string team:references seasons:integer
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here, in addition to the bundling gems, we added &lt;a href=&quot;https://github.com/faker-ruby/faker&quot;&gt;Faker&lt;/a&gt; to our project to allow us to quickly add seed data to the database and added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Team&lt;/code&gt; model and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; resource to our project.&lt;/p&gt;

&lt;p&gt;The core of our application will be a list of players, we won’t interact with Teams directly so we skip adding a controller and views for Teams.&lt;/p&gt;

&lt;p&gt;Next up, we’ll use the new bundling gems to install webpack to handle our JavaScript and &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind&lt;/a&gt; for CSS. From your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails javascript:install:webpack
rails css:install:tailwind&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we’ll wrap up setup by installing Hotwire in our application. Technically, we only need Turbo for this project, but having Stimulus setup won’t hurt anything. One more time, from your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails hotwire:install&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With everything installed, you can start up the server and build your assets with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; from your terminal.&lt;/p&gt;

&lt;h2 id=&quot;build-table-layout&quot;&gt;Build table layout&lt;/h2&gt;
&lt;p&gt;We’ll begin by updating the players index page to display a nicely styled table of all of the players in the database. Rails’ scaffolding gets us most of the way there, but instead of rendering the players table in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html.erb&lt;/code&gt; we’ll move the table to a partial.&lt;/p&gt;

&lt;p&gt;This partial will come in handy later when we use Turbo Frames to sort the list of players.&lt;/p&gt;

&lt;p&gt;First, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/index.html.erb&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;max-w-7xl mx-auto mt-12&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;players: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The index is now rendering a partial (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt;) that hasn’t been created yet, so we’ll create that partial next.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/_players.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill the partial in like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;shadow overflow-hidden rounded border-b border-gray-200&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 bg-white&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Team&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Seasons&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;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;players&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;player&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&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&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;/td&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;team&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;/td&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seasons&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;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;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This partial simply renders a table that contains a header row and then rows for each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; variable. The classes are all built-in Tailwind classes that are not integral to the function of the application.&lt;/p&gt;

&lt;p&gt;Now we’ve got a nice looking table ready to display our players, but no players in the database! Let’s fix that by creating enough seed data that we can see sorting in action.&lt;/p&gt;

&lt;p&gt;First, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db/seeds.rb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Dallas Mavericks&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;LA Clippers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;LA Lakers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;San Antonio Spurs&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Boston Celtics&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Miami Heat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Orleans Pelicans&apos;&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;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Team&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;nb&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;mi&quot;&gt;100&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;no&quot;&gt;Player&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;no&quot;&gt;Faker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Name&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;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Team&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;no&quot;&gt;Team&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;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sample&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;seasons: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&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;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then from your terminal, seed the database:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails db:seed&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If you haven’t already, boot up the app and build assets with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt; from your terminal, and then head to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;localhost:3000/players&lt;/a&gt; and see the list of randomly generated players, ready to sort.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-20-preview-screenshot.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of a data table with columns for name, team, and seasons&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now that we’ve got our table populated and displaying, let’s start on the fun stuff by making the table sortable with Turbo Frames.&lt;/p&gt;

&lt;h2 id=&quot;add-turbo-frame-sorting&quot;&gt;Add turbo frame sorting&lt;/h2&gt;
&lt;p&gt;As a reminder, our goal is for users to be able to click on a table header to sort the table by that column. We want sorting to happen without a page turn, and we want to use a Turbo Frame to update the list of players in the UI.&lt;/p&gt;

&lt;p&gt;To start, we’ll convert the wrapper div in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; with an id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; and the same classes that the wrapper div had. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players/_players.html.erb&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;players&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;shadow overflow-hidden rounded border-b border-gray-200&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;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 bg-white&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;!-- snip --&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/table&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Don’t forget to replace the closing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;/div&amp;gt;&lt;/code&gt; with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;% end %&amp;gt;&lt;/code&gt; tag!&lt;/p&gt;

&lt;p&gt;Now that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; turbo frame wraps the content of the table, &lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames&quot;&gt;links inside of this frame&lt;/a&gt; will automatically attempt to replace the content of the turbo frame with the content they receive from the server.&lt;/p&gt;

&lt;p&gt;We’ll come back to this concept in a minute, but first, let’s build out the remainder of the code we need for the first pass at sorting the table.&lt;/p&gt;

&lt;p&gt;Next, inside of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial still, update the table header like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;thead&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &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;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Seasons&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;/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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve replaced the static text labeling each column with a call to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; helper method, passing in a column and a label for each header cell.&lt;/p&gt;

&lt;p&gt;This helper method doesn’t exist yet, so let’s add that next. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;PlayersHelper&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&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;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; renders a standard Rails &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt;, using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;label&lt;/code&gt; to display the link to the user, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;column&lt;/code&gt; to append a parameter to the generated URL.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt; points to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list_players_path&lt;/code&gt;, which we haven’t defined yet. Let’s do that next, in routes.rb:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:players&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;collection&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;s1&quot;&gt;&apos;list&apos;&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And finally, we need to update our controller to define an action for the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; route we’ve added.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/controllers/players_controller.rb&lt;/code&gt;, add a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; method, like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;:column&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; asc&quot;&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve defined a new controller action that queries the database for all of the players, includes the teams table in the query, and orders the players using the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params[:column]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;includes(:team)&lt;/code&gt; is necessary both to allow sorting players by their team name and for performance, since the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial makes a call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player.team.name&lt;/code&gt; to display the players name on each row.&lt;/p&gt;

&lt;p&gt;Once the players are retrieved, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial is rendered and sent back to the browser.&lt;/p&gt;

&lt;p&gt;With the controller update in place, refresh the page, click on a column header, and, if all has gone well, you should see that the list of players is updated (almost) instantly with the correct column sorting applied.&lt;/p&gt;

&lt;p&gt;When you sort by a column, you should see output in your server logs like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;Started GET &lt;span class=&quot;s2&quot;&gt;&quot;/players/list?column=seasons&quot;&lt;/span&gt;
Processing by PlayersController#list as HTML
  Parameters: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;column&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
  Player Load &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.5ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  SELECT &lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;.&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; FROM &lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt; ORDER BY seasons asc
  ↳ app/views/players/_players.html.erb:18
  Team Load &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.2ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  SELECT &lt;span class=&quot;s2&quot;&gt;&quot;teams&quot;&lt;/span&gt;.&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; FROM &lt;span class=&quot;s2&quot;&gt;&quot;teams&quot;&lt;/span&gt; WHERE &lt;span class=&quot;s2&quot;&gt;&quot;teams&quot;&lt;/span&gt;.&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt; IN &lt;span class=&quot;o&quot;&gt;(&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;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;, 3], &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;, 1], &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;, 2], &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;, 6], &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;, 4], &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;, 5], &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;, 7]]
  ↳ app/views/players/_players.html.erb:18
  Rendered players/_players.html.erb &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Duration: 5.9ms | Allocations: 3994&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Completed 200 OK &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;7ms &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Views: 5.6ms | ActiveRecord: 0.6ms | Allocations: 4224&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Nice work so far, let’s pause here to step back and talk about how this all ties together.&lt;/p&gt;

&lt;p&gt;We started by wrapping the players table in a Turbo Frame with an id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt;. Then we added a link to each of the header cells, pointing to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players/list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because the link is wrapped in a turbo frame, when the response is sent back from the server, Turbo scans the response for a turbo frame with the same id (players) and replaces the existing frame content with the new frame content.&lt;/p&gt;

&lt;p&gt;In our case, this means that the updated list of players, rendered by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players/list&lt;/code&gt;, replaces the content of the players table without touching the rest of the page.&lt;/p&gt;

&lt;p&gt;This use of Turbo Frames, to replace the content of a matching frame, is the simplest approach to using Turbo Frames.&lt;/p&gt;

&lt;p&gt;In more advanced cases, we can have a link inside of a frame &lt;a href=&quot;https://turbo.hotwired.dev/handbook/frames#targeting-navigation-into-or-out-of-a-frame&quot;&gt;break out of a frame&lt;/a&gt;, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target=_top&lt;/code&gt; or use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-frame&lt;/code&gt; to tell Turbo that a link from outside of a frame should target that frame.&lt;/p&gt;

&lt;p&gt;Turbo Frames enable us to make fast, efficient page updates without adding significant complexity to our code.&lt;/p&gt;

&lt;p&gt;Now that we’ve got a little more context for how Turbo Frames are enabling sorting in our application, let’s finish up by adding the ability to sort in both ascending and descending order and inserting a visual indicator when sorting is applied.&lt;/p&gt;

&lt;h2 id=&quot;add-descending-sorting&quot;&gt;Add descending sorting&lt;/h2&gt;
&lt;p&gt;We want users to be able to sort in descending order by clicking on the same column header twice in a row. The first click will sort in ascending order, the next click will sort in descending order.&lt;/p&gt;

&lt;p&gt;Here’s a demonstration of the desired user experience for this section of the article:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-20-without-indicators.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on column headers to sort a data table in ascending and descending order&quot; /&gt;&lt;/p&gt;

&lt;p&gt;First, we’ll modify the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; helper that we added in the last section to send a direction as well as a column in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; request.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;label&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;column&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;:column&lt;/span&gt;&lt;span class=&quot;p&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;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;direction: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;next_direction&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;link_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;label&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;list_players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;direction: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;asc&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;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;next_direction&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;:direction&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;s1&quot;&gt;&apos;asc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;desc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_link&lt;/code&gt; now adds a second parameter into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt;. The new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; parameter is set to the value of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next_direction&lt;/code&gt; helper method if we are generating the sort link for a column that is being used for sorting; otherwise, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; is set to ascending.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next_direction&lt;/code&gt; simply inverts the current sort direction so the sort direction changes with each click.&lt;/p&gt;

&lt;p&gt;Next we’ll update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;list&lt;/code&gt; method in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players_controller.rb&lt;/code&gt;  to use the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; parameter in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt; clause:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;list&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;:column&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; &lt;/span&gt;&lt;span class=&quot;si&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;:direction&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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# snip&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With this change in place, we can now sort our table in either direction. Incredible stuff, nice work making it this far!&lt;/p&gt;

&lt;p&gt;One more set of changes left: Giving users feedback in the UI that sorting is active.&lt;/p&gt;

&lt;h2 id=&quot;add-visual-indicator-of-sort-direction&quot;&gt;Add visual indicator of sort direction&lt;/h2&gt;
&lt;p&gt;Our final task is to add an indicator next to the column name when that column is being used to sort the table. The indicator will be a small triangle, pointing up when sorting in ascending order and down when sorting in descending order.&lt;/p&gt;

&lt;p&gt;We’ll start by updating the table header in the players partial like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;thead&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&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;:column&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;s2&quot;&gt;&quot;name&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &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;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&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;:column&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;s2&quot;&gt;&quot;teams.name&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;teams.name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer relative&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;sort_indicator&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&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;:column&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;s2&quot;&gt;&quot;seasons&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;sort_link&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;column: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;seasons&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;label: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Seasons&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;/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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve added a conditional call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_indicator&lt;/code&gt; into each header cell.&lt;/p&gt;

&lt;p&gt;Since we only want to add the indicator when the column is being used to sort the table, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if params[:column]&lt;/code&gt; to skip the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_indicator&lt;/code&gt; call on inactive columns.&lt;/p&gt;

&lt;p&gt;We also added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;relative&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;th&amp;gt;&lt;/code&gt; elements class lists because the sort indicator will be absolutely positioned inside of the header cell.&lt;/p&gt;

&lt;p&gt;Next up, we’ll define &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_indicator&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/helpers/players_helper.rb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort_indicator&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;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;sort sort-&lt;/span&gt;&lt;span class=&quot;si&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;:direction&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;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_indicator&lt;/code&gt; simply returns a span with a class that matches the current sort direction, read from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params[:direction]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Finally, we’ll add css so our sort indicator isn’t just an invisible span on the page.&lt;/p&gt;

&lt;p&gt;For convenience, we’ll insert the css right into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/assets/stylesheets/application.css&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span class=&quot;nc&quot;&gt;.sort&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;1rem&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;0.5rem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;width&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;nl&quot;&gt;height&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;nl&quot;&gt;border-left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;6px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&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;nl&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;6px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&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;span class=&quot;nc&quot;&gt;.sort-desc&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&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;nc&quot;&gt;.sort-asc&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-bottom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With the CSS added and our new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_indicator&lt;/code&gt; helper defined, refresh the page, click on the column headers, and see that the table is sorted and a visual indicator is shown for the current column and sort direction:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-20-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on column headers on a data table. With each click, the table sorts itself by that column and a triangle indicator appears next to the column used for sorting&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Great job following along, we’ve reached the end of the code for today!&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we learned how to build a sortable table with Rails and Turbo Frames. Our table can be sorted without a page turn, and we built the interface using basic set of tools familiar to any Rails developer.&lt;/p&gt;

&lt;p&gt;While our table only supports sorting today, our frame-based approach can be enhanced to support filtering and searching without deviating from the core concept of using Turbo Frames to display and update the table.&lt;/p&gt;

&lt;p&gt;Turbo Frames, along with the rest of the &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire stack&lt;/a&gt;, give Rails developers the ability to quickly build fast, modern user experiences without adding the weight and complexity that can come with JavaScript frameworks.&lt;/p&gt;

&lt;p&gt;You’ll note that I did not compare the approach in this article directly to the approach in yesterday’s article where we built the same UI with StimulusReflex. Rather than attempting a direct, side-by-side comparison, I prefer presenting both options as standalone projects that show how to build simple experiences using each tool so that readers can learn how each tool works, begin experimenting, and see which feels right.&lt;/p&gt;

&lt;p&gt;Both &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;StimulusReflex&lt;/a&gt; and Turbo (Frames and Streams) can deliver world-class user experiences in production applications while providing an unbeatable developer experience. The right choice for you and your team is almost certain to be the option that your team feels most comfortable with and most productive in.&lt;/p&gt;

&lt;p&gt;Whichever routet you choose, with the Hotwire stack, StimulusReflex, and CableReady available, Rails is well-positioned for the future.&lt;/p&gt;

&lt;p&gt;If you’re ready to go further with on your Turbo journey, here are a few places to start:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Review the Turbo &lt;a href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Dive in to the Turbo Rails &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;source code&lt;/a&gt; on Github&lt;/li&gt;
  &lt;li&gt;Spend time with &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus&lt;/a&gt;, the other side of the Hotwire stack&lt;/li&gt;
  &lt;li&gt;Learn from the &lt;a href=&quot;https://discuss.hotwired.dev/&quot;&gt;community&lt;/a&gt; in the Hotwire discussion forum&lt;/li&gt;
  &lt;li&gt;Join the &lt;a href=&quot;https://discord.gg/stimulus-reflex&quot;&gt;StimulusReflex discord&lt;/a&gt;, where folks will happily help you learn more about building reactive web applications with Rails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for today. As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Sort tables (almost) instantly with Ruby on Rails and StimulusReflex</title>
   <link href="https://colby.so/posts/a-sortable-table-with-rails-and-stimulusreflex"/>
   <updated>2021-09-18T00:00:00+00:00</updated>
   <id>https://colby.so/posts/a-sortable-table-with-rails-and-stimulusreflex</id>
   <content type="html">&lt;p&gt;Today we’re going to use Ruby on Rails and &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;StimulusReflex&lt;/a&gt; to build a table that sorts itself each time a user clicks on a header column.&lt;/p&gt;

&lt;p&gt;Sorting will occur without a page turn, in less than 100ms, won’t require any custom JavaScript, and we’ll build the whole thing with regular old ERB templates and a little bit of Ruby.&lt;/p&gt;

&lt;p&gt;The end result will be very fast, efficient, simple to reason about, and easy to extend as new functionality is required.&lt;/p&gt;

&lt;p&gt;It’ll be pretty fancy.&lt;/p&gt;

&lt;p&gt;When we’re finished, it will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-19-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on column headers on a data table. With each click, the table sorts itself by that column and a triangle indicator appears next to the column used for sorting&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You can view the finished product &lt;a href=&quot;https://stark-peak-59471.herokuapp.com/&quot;&gt;on Heroku&lt;/a&gt;, or find the full source on &lt;a href=&quot;https://github.com/DavidColby/sortable_players/tree/sortable&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article will be most useful to folks who are familiar with Ruby on Rails, but you will not need any previous experience with Stimulus or StimulusReflex to follow along. If you’ve never worked with Rails before, some concepts here may be a little tough to follow.&lt;/p&gt;

&lt;p&gt;Let’s get started!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;As usual, we’ll start with a brand new Rails 6.1 application, with Tailwind and StimulusReflex installed. Tailwind is not a requirement, but it helps us make our table look a little nicer and the extra setup time is worth the cost.&lt;/p&gt;

&lt;p&gt;If you’d like to skip the copy/pasting setup steps, you can clone down the example &lt;a href=&quot;https://github.com/DavidColby/sortable_players/tree/main&quot;&gt;repo&lt;/a&gt; and skip ahead to the &lt;a href=&quot;#building-the-table&quot;&gt;Building the Table section&lt;/a&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; branch of the repo is pinned to the end of the setup process and ready for you to start writing code.&lt;/p&gt;

&lt;p&gt;If you’re going to follow along with the setup, first, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new player_sorting &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;player_sorting
bundle add stimulus_reflex
bundle add faker
rake stimulus_reflex:install
rails g model Team name:string
rails g scaffold Player name:string team:references seasons:integer
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Assuming you’ve got Rails and Yarn installed, this will produce a brand new Rails 6.1 application (at the time of this writing), install StimulusReflex, and scaffold up the Team and Player models that we’ll use to build our sortable table.&lt;/p&gt;

&lt;p&gt;If you don’t care to use Tailwind for this article, feel free to skip past this next section, Tailwind is a convenient way to make things look presentable, but if you just want to focus on sorting the table without any styling, Tailwind is not necessary!&lt;/p&gt;

&lt;p&gt;If you want to install Tailwind, start in your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;yarn add tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init
&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;app/javascript/stylesheets
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/javascript/stylesheets/application.scss&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tailwind.config.js&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;module.exports &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  purge: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;&apos;./app/**/*/*.html.erb&apos;&lt;/span&gt;,
    &lt;span class=&quot;s1&quot;&gt;&apos;./app/helpers/**/*/*.rb&apos;&lt;/span&gt;,
    &lt;span class=&quot;s1&quot;&gt;&apos;./app/javascript/**/*/*.js&apos;&lt;/span&gt;,
    &lt;span class=&quot;s1&quot;&gt;&apos;./app/javascript/**/*/*.vue&apos;&lt;/span&gt;,
    &lt;span class=&quot;s1&quot;&gt;&apos;./app/javascript/**/*/*.react&apos;&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;,
  darkMode: &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;,
  theme: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    extend: &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt;,
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;,
  variants: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    extend: &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt;,
  &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;,
  plugins: &lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt;,
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And postcss.config.js:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// postcss.config.js&lt;/span&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;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;nf&quot;&gt;require&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;tailwindcss&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;./tailwind.config.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;nf&quot;&gt;require&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;postcss-import&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;nf&quot;&gt;require&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;postcss-flexbugs-fixes&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;nf&quot;&gt;require&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;postcss-preset-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;na&quot;&gt;autoprefixer&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;flexbox&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;no-2009&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;na&quot;&gt;stage&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next we’ll update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascripts/stylesheets/application.scss&lt;/code&gt; to import Tailwind:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-scss&quot; data-lang=&quot;scss&quot;&gt;&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tailwindcss/base&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;s2&quot;&gt;&quot;tailwindcss/components&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;s2&quot;&gt;&quot;tailwindcss/utilities&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then include that stylesheet in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascripts/packs/application.js&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;../stylesheets/application.scss&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, update the application layout to include the stylesheet generated by webpack:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stylesheet_pack_tag&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;application&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;data-turbolinks-track&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;reload&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;media: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;all&apos;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Whew. We’re through the setup and ready to start building.&lt;/p&gt;

&lt;h2 id=&quot;building-the-table&quot;&gt;Building the table&lt;/h2&gt;
&lt;p&gt;First up, let’s get our bearings.&lt;/p&gt;

&lt;p&gt;The core of our application is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; resource. We are going to construct a table that displays all of the players in our database, with the players name, their team’s name, and their seasons played as columns.&lt;/p&gt;

&lt;p&gt;We’ll only use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Team&lt;/code&gt; model created during setup in the context of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt;, so we don’t need a controller or views for teams.&lt;/p&gt;

&lt;p&gt;We’ll start by moving the table from the players index view to a partial. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/_players.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that partial in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;players&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;shadow overflow-hidden rounded border-b border-gray-200&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 bg-white&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bg-gray-800 text-white&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;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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Team&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&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;Seasons&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;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;players&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;player&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&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&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;/td&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;team&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;/td&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;td&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-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;n&quot;&gt;player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;seasons&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;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;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Most of the markup here is Tailwind classes for styling the table.&lt;/p&gt;

&lt;p&gt;The functionally important pieces are the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ids&lt;/code&gt; set on the wrapper div (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#players&lt;/code&gt;) and the ids set on the table header cells. These ids will be used later to update the DOM when the user clicks to sort the table.&lt;/p&gt;

&lt;p&gt;Next update the index view to use the new partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;max-w-7xl mx-auto mt-12&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;players: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With these changes in place, we have a nice looking table ready to display the players in the database, but we don’t have any players. Since we’re going to be sorting, let’s make sure the database has plenty of data in it.&lt;/p&gt;

&lt;p&gt;Copy this into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db/seeds.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Dallas Mavericks&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;LA Clippers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;LA Lakers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;San Antonio Spurs&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Boston Celtics&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Miami Heat&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Orleans Pelicans&apos;&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;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Team&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;nb&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;mi&quot;&gt;100&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;no&quot;&gt;Player&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;no&quot;&gt;Faker&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Name&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;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Team&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;no&quot;&gt;Team&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;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sample&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;seasons: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&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;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails db:seed&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now start up your Rails server and head to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;localhost:3000/players&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If all has gone well, you should see a table populated with 100 randomly generated players.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-19-preview-screenshot.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of a data table with columns for name, team, and seasons&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Next up, we’ll build our sorting mechanism with StimulusReflex.&lt;/p&gt;

&lt;h2 id=&quot;sorting-with-stimulusreflex&quot;&gt;Sorting with StimulusReflex&lt;/h2&gt;
&lt;p&gt;To sort the table, we’re going to create a &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflex-classes&quot;&gt;reflex class&lt;/a&gt; with a single action, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the user clicks on a table header, we’ll call the reflex action to sort the players and update the DOM.&lt;/p&gt;

&lt;p&gt;We’ll start by generating a new Reflex. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails g stimulus_reflex Table&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This generator creates both a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reflex&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/reflexes&lt;/code&gt; and a related Stimulus controller in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascripts/controllers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For this article, we won’t make any modifications to the Stimulus controller, so we’ll focus on the Ruby reflex found at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/reflexes/table_reflex.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Fill that reflex in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TableReflex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationReflex&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;direction&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;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#players&apos;&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The first line of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; is a standard Rails ActiveRecord query. In it, we retrieve all of the players from the database, ordered by attributes sent from the DOM when the reflex action is triggered.&lt;/p&gt;

&lt;p&gt;Reflex actions have access to a &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflex-classes#building-your-reflex-action&quot;&gt;variety of properties&lt;/a&gt;. In our case, the property we’re interested in is &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflex-classes#element&quot;&gt;element&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt; is a representation of the DOM element that triggered the reflex and it includes all of the data attributes set on that element, accessible via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element.dataset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means that in reflex actions, we can always access data attributes from the element that triggered the reflex as if we were working with that element in JavaScript. Handy.&lt;/p&gt;

&lt;p&gt;For our purposes, we care about two data elements that don’t yet exist in the DOM — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;column&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt;. The ActiveRecord query to retrieve and order players uses those values to know which column to order the results by, and in which direction (ascending or descending) the results should be ordered.&lt;/p&gt;

&lt;p&gt;After we’ve retrieved the ordered list of players from the database, we use a &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs&quot;&gt;selector morph&lt;/a&gt; to update the DOM, replacing the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial we created earlier with the updated list of players.&lt;/p&gt;

&lt;p&gt;Our reflex is built, but there’s no way for a user to trigger the reflex. Let’s add that next.&lt;/p&gt;

&lt;p&gt;In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial, update the header row like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-name&quot;&lt;/span&gt; 
    &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-reflex=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click-&amp;gt;Table#sort&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-column=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-direction=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;asc&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;Name&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-team&quot;&lt;/span&gt; 
    &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-reflex=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click-&amp;gt;Table#sort&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-column=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;teams.name&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-direction=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;asc&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;Team&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-seasons&quot;&lt;/span&gt; 
    &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-left py-3 px-6 uppercase font-semibold text-sm hover:cursor-pointer&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-reflex=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;click-&amp;gt;Table#sort&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-column=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;seasons&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;data-direction=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;asc&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;Seasons&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve updated each header cell with the three data attributes we need for the reflex to be triggered and to run successfully.&lt;/p&gt;

&lt;p&gt;First, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-reflex&lt;/code&gt; is used to &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflexes#declaring-a-reflex-in-html-with-data-attributes&quot;&gt;tell StimulusReflex&lt;/a&gt; that this element should trigger a reflex when some action occurs. In our case, it will be called on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;click&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each cell also gets a unique &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-column&lt;/code&gt; value, which we use to sort the players result by the matching database column. Finally, each header cell also starts with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asc&lt;/code&gt; which is used to set the direction of the order query.&lt;/p&gt;

&lt;p&gt;Let’s look at the Reflex code again and review what’s happening now that we’ve updated the DOM to call this reflex.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;direction&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;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#players&apos;&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; method we’ve defined matches the name of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-reflex&lt;/code&gt; on each header cell. When the header cell is clicked, this reflex will run.&lt;/p&gt;

&lt;p&gt;The header cell that that the user clicks will be passed to the reflex as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt;, giving us access to the element’s data attributes, which we access through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element.dataset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; is set by the database query, we use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selector&lt;/code&gt; &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs&quot;&gt;morph&lt;/a&gt; to tell the browser to update the element with the id of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; with the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial, using the updated, reordered list of players.&lt;/p&gt;

&lt;p&gt;This is the magic of StimulusReflex in action. With just a couple of data attributes and a few lines of simple Ruby code, our users can now click on a table header and, in &amp;lt; 100ms, they’ll see a table sorted to match their request.&lt;/p&gt;

&lt;p&gt;Refresh the page and try it out for yourself. If all has gone well, clicking on a header cell should sort the table by that column in ascending order. While this is nice, we have a few more items to address before our work is complete.&lt;/p&gt;

&lt;p&gt;Next up, we’ll address an error with the order query, and then finish this article by modifying the sort reflex to allow users to sort in both ascending and descending order and display visual feedback to indicate what column is being sorted.&lt;/p&gt;

&lt;h2 id=&quot;fixing-an-ordering-error&quot;&gt;Fixing an ordering error&lt;/h2&gt;
&lt;p&gt;First, sharp eyed readers might have noticed that sorting by the team name column doesn’t work yet.&lt;/p&gt;

&lt;p&gt;Each header cell’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;column&lt;/code&gt; data attribute matches a column in the database, so we can generate the order query dynamically. This works fine for the name and season because those columns live on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Players&lt;/code&gt; table. ActiveRecord knows how to order by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seasons&lt;/code&gt; without any extra effort.&lt;/p&gt;

&lt;p&gt;For the team name column, we’re passing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;teams.name&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt; call in our query, which ActiveRecord trips over with an error like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;c&quot;&gt;# The text will vary depending on the database adapter you&apos;re using!&lt;/span&gt;
Reflex Table#sort failed: SQLite3::SQLException: no such column: teams.name&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We can fix this by updating the query slightly:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;:team&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;direction&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;includes(:team)&lt;/code&gt; to the existing order query, making the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Teams&lt;/code&gt; table accessible in the order clause and fixing the “no such column” error that was thrown when attempting to sort by team name.&lt;/p&gt;

&lt;p&gt;Note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;joins&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;includes&lt;/code&gt; would also fix the error, but since we need to use team name when we render the players partial (to display each player’s team), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;includes&lt;/code&gt; is the better choice.&lt;/p&gt;

&lt;p&gt;Before moving on, you’ll notice that we are using user-accessible values to generate a SQL query — anyone can modify data-attributes in their browser’s dev tools.&lt;/p&gt;

&lt;p&gt;Prior to Rails 6, this could have opened up our application to SQL injection; however, &lt;a href=&quot;https://github.com/rails/rails/pull/27947&quot;&gt;since Rails 6&lt;/a&gt;, Rails will raise an error automatically if anything but a table/column name + a sort direction are passed in to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;adding-descending-ordering&quot;&gt;Adding descending ordering&lt;/h2&gt;
&lt;p&gt;With the work we’ve done so far, sorting the table works great as long as the user only wants to sort in ascending (A - Z) order. Since the sort direction is read from a data attribute that is always “asc”, there is no way to sort in descending (Z - A) order. Let’s add that functionality next.&lt;/p&gt;

&lt;p&gt;Before jumping in to the code, let’s outline the desired user experience.&lt;/p&gt;

&lt;p&gt;When a user clicks on a column header, the table should be sorted by that column, in ascending order. When the user clicks on the same column header again, the table should be sorted by that column in descending order. And then we alternate, forever, between ascending and descending on subsequent clicks.&lt;/p&gt;

&lt;p&gt;Sorting by a different column should always sort in ascending order on the first click.&lt;/p&gt;

&lt;p&gt;Here’s a gif of what we’re aiming for:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-19-asc-desc-without-carets.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on column headers to sort a data table in ascending and descending order&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To achieve the desired user experience, we need to track the next sort direction for each header cell, so that when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex is called, the right direction can be sent to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order&lt;/code&gt; query.&lt;/p&gt;

&lt;p&gt;To do this, we’re going to take advantage of &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/cableready&quot;&gt;CableReady’s&lt;/a&gt; tight integration with StimulusReflex. We’ll update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex to include a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_ready&lt;/code&gt; &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/operations&quot;&gt;operation&lt;/a&gt; that changes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; data attribute of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt; that triggered the reflex.&lt;/p&gt;

&lt;p&gt;To do this, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TableReflex&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# snip&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;set_sort_direction&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;next_direction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;direction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;direction&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;desc&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;asc&apos;&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;set_sort_direction&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;cable_ready&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_dataset_property&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;selector: &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;element&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;&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;s1&quot;&gt;&apos;direction&apos;&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;n&quot;&gt;next_direction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;direction&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve added two private methods to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TableReflex&lt;/code&gt; class. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next_direction&lt;/code&gt; is a simple helper method that takes the current value of direction and returns the next value.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_sort_direction&lt;/code&gt; is more interesting. In it, we use CableReady’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_dataset_property&lt;/code&gt; &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/reference/operations/element-mutations#set_dataset_property&quot;&gt;operation&lt;/a&gt; to set the value of the element’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; data attribute to the value of  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next_direction&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;Finally, we call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_sort_direction&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex, which adds the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_dataset_property&lt;/code&gt; operation to the queue each time the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex runs.&lt;/p&gt;

&lt;p&gt;With this in place, refresh and click on the same column multiple times to see that each click reorders the table, toggling between ascending and descending order.&lt;/p&gt;

&lt;h2 id=&quot;order-of-operations-not-just-for-math-class&quot;&gt;Order of operations: Not just for math class&lt;/h2&gt;

&lt;p&gt;Before moving on, it is important to pause and think about how this code works. When a reflex includes CableReady operations, a &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/cableready#order-of-operations&quot;&gt;specific order of operations&lt;/a&gt; is always followed.&lt;/p&gt;

&lt;p&gt;First, &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/methods&quot;&gt;broadcasted&lt;/a&gt; CableReady &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;operations&lt;/code&gt; execute. Next, StimulusReflex &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morphs&lt;/code&gt; execute. Finally, CableReady &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;operations&lt;/code&gt; that are not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcasted&lt;/code&gt; execute (that’s our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_dataset_property&lt;/code&gt; operation).&lt;/p&gt;

&lt;p&gt;Because the StimulusReflex morph runs before the CableReady operation, each table header cell has its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; data attribute reset to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asc&lt;/code&gt; when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; is triggered. This behavior lets us “reset” sort directions when moving between columns without having to add logic in the partial.&lt;/p&gt;

&lt;p&gt;Immediately &lt;strong&gt;after&lt;/strong&gt; the StimulusReflex morph, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_dataset_property&lt;/code&gt; runs and updates the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; on the currently active sort column.&lt;/p&gt;

&lt;p&gt;If we appended &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.broadcast&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_dataset_property&lt;/code&gt; operation, the direction property would be updated &lt;strong&gt;before&lt;/strong&gt; the StimulusReflex &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morph&lt;/code&gt;, causing the CableReady update to be overwritten by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morph&lt;/code&gt;, breaking the ability to sort in descending order.&lt;/p&gt;

&lt;p&gt;This order of operations is important to understand, and helps unlock a new level of functionality within reflexes.&lt;/p&gt;

&lt;p&gt;Before moving on, now that we understand the order of operations in reflex actions, we can use that knowledge to make a small optimization to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex.&lt;/p&gt;

&lt;p&gt;We know that every time the reflex runs, each header cell will have its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-direction&lt;/code&gt; value set to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asc&lt;/code&gt; before the active sort column is updated by the CableReady &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_dataset_property&lt;/code&gt; operation.&lt;/p&gt;

&lt;p&gt;Since the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-direction&lt;/code&gt; is already &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asc&lt;/code&gt;, if the next sort direction is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asc&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_dataset_property&lt;/code&gt; won’t do anything useful.&lt;/p&gt;

&lt;p&gt;Let’s update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; to skip the CableReady operation in that case:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# snip&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;set_sort_direction&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next_direction&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;direction&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;s1&quot;&gt;&apos;desc&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;set_sort_direction&lt;/code&gt; will only be run when necessary, simplifying our DOM updates at the cost of slightly more complexity in our ruby code.&lt;/p&gt;

&lt;p&gt;Let’s finish up our sortable table implementation by adding a visual indicator to the table when sorting is active.&lt;/p&gt;

&lt;h2 id=&quot;sort-direction-visuals&quot;&gt;Sort direction visuals&lt;/h2&gt;
&lt;p&gt;To indicate which column is being sorted, and in which direction, we’ll draw a triangle with CSS, with an upward pointing triangle indicating ascending order, and a downward triangle indicating descending order.&lt;/p&gt;

&lt;p&gt;Only the column that is being used for sorting will display the icon.&lt;/p&gt;

&lt;p&gt;When we’re finished, the indicator will look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-19-sort-indicator.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of a data table with a triangle pointing upward positioned to the left of a column header labeled Team, indicating the table is sorted in ascending order by team name&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Let’s start with the CSS.&lt;/p&gt;

&lt;p&gt;We’ll insert the CSS directly into our main &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application.scss&lt;/code&gt; file to keep things simple:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span class=&quot;nc&quot;&gt;.sort&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;5px&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;width&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;nl&quot;&gt;height&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;nl&quot;&gt;border-left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;6px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&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;nl&quot;&gt;border-right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;6px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&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;span class=&quot;nc&quot;&gt;.sort-desc&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&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;nc&quot;&gt;.sort-asc&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;border-bottom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8px&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;solid&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;#fff&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We’ll use the base &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sort&lt;/code&gt; class along with a dynamic &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sort-asc&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sort-desc&lt;/code&gt; to display the sort indicator. If you’re interested in how this CSS works, &lt;a href=&quot;https://www.freecodecamp.org/news/css-shapes-explained-how-to-draw-a-circle-triangle-and-more-using-pure-css/&quot;&gt;this is a nice introduction&lt;/a&gt; to drawing shapes with CSS.&lt;/p&gt;

&lt;p&gt;With the CSS ready, next we’ll create a partial to render the indicator, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/_sort_indicator.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;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;sort sort-&amp;lt;%= direction %&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Nothing fancy here, just appending the local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direction&lt;/code&gt; variable to the class name to ensure our triangle points in the right direction.&lt;/p&gt;

&lt;p&gt;We’ll finish up by updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex to insert the sort indicator into the DOM, again relying on a CableReady operation that runs immediately after the StimulusReflex &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morph&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TableReflex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationReflex&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sort&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# snip&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;insert_indicator&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;c1&quot;&gt;# snip&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;insert_indicator&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cable_ready&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prepend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;selector: &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;element&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;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;render&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;s1&quot;&gt;&apos;players/sort_indicator&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;direction: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;direction&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;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added another private method to the reflex to handle inserting the sort indicator when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex is called.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;insert_indicator&lt;/code&gt; uses CableReady’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prepend&lt;/code&gt; &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/operations/dom-mutations#prepend&quot;&gt;operation&lt;/a&gt; to insert the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort_indicator&lt;/code&gt; partial into the DOM, before the target element’s first child.&lt;/p&gt;

&lt;p&gt;With this in place, we can refresh the page and see the sort indicator added each time the sort reflex runs, pointing up for ascending sorts and down for descending sorts:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-19-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on column headers on a data table. With each click, the table sorts itself by that column and a triangle indicator appears next to the column used for sorting&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;an-implementation-note&quot;&gt;An implementation note&lt;/h2&gt;

&lt;p&gt;During this article I made a choice to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_ready&lt;/code&gt; operations to add the sort indicator and update the data attribute.&lt;/p&gt;

&lt;p&gt;Instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_ready&lt;/code&gt; operations, another approach would be to assign instance or local variables for things like “active column” and “next direction” during the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sort&lt;/code&gt; reflex. We could then read those variables when rendering the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial, using them to render the sort indicator and set the next sort direction.&lt;/p&gt;

&lt;p&gt;This approach would allow us to eliminate the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_ready&lt;/code&gt; operations; however, doing so would complicate the view. Either approach is fine, my personal preference is to rely on the very fast &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_ready&lt;/code&gt; operations to simplify the view.&lt;/p&gt;

&lt;p&gt;Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_ready&lt;/code&gt; also has the added benefit of letting us talk more about how StimulusReflex works, which is a bonus in a tutorial article like this one. As you spend more time with StimulusReflex, experiment with different approaches and find what works best for you.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we built a sortable table interface with Ruby on Rails, StimulusReflex, and CableReady. Our table is fast, updates efficiently, is easy to extend, and is no where near production ready yet. What we built today was part one of a two part series. Next up, we’ll extend the sortable table by adding filtering and pagination, getting closer to the full-featured implementation seen in &lt;a href=&quot;https://beastmode.leastbad.com/&quot;&gt;Beast Mode&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While there are numerous ways to implement a sortable table interface, for Rails developers, StimulusReflex is worthy of strong consideration. SR’s fast, efficient mechanisms for updating and re-rendering the DOM, including &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs&quot;&gt;bypassing ActionDispatch’s overhead&lt;/a&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;selector&lt;/code&gt; morphs, allow us to sort and render the updated table extremely quickly, with minimal code complexity or additional mental overhead. Its tight integrations with CableReady and Stimulus combine into an extremely powerful tool in any Rails developers kit.&lt;/p&gt;

&lt;p&gt;To go further into StimulusReflex and CableReady:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Review &lt;a href=&quot;https://www.stimulusreflexpatterns.com/patterns/&quot;&gt;StimulusReflex patterns&lt;/a&gt; for thoughtfully designed solutions in StimulusReflex, including &lt;a href=&quot;https://www.stimulusreflexpatterns.com/patterns/filterable_reflex/&quot;&gt;filterable&lt;/a&gt; for working with complex sorting and filtering requirements&lt;/li&gt;
  &lt;li&gt;Join the &lt;a href=&quot;https://discord.com/invite/stimulus-reflex&quot;&gt;StimulusReflex discord&lt;/a&gt; and connect with other folks building cool stuff with StimulusReflex, CableReady, and Rails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for today, as always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Server-rendered modal forms on Rails with CableReady, Mrujs, Stimulus, and Tailwind</title>
   <link href="https://colby.so/posts/modal-forms-with-cableready-and-mrujs"/>
   <updated>2021-09-09T00:00:00+00:00</updated>
   <id>https://colby.so/posts/modal-forms-with-cableready-and-mrujs</id>
   <content type="html">&lt;p&gt;The Rails ecosystem continues to thrive, and Rails developers have all the tools they need to build modern, reactive, scalable web applications quickly and efficiently. If you care about delivering exceptional user experiences, your options in Rails-land have never been better.&lt;/p&gt;

&lt;p&gt;Today we’re going to dive into this ecosystem to use two cutting edge Rails projects to allow users to submit forms that are rendered inside of a modal.&lt;/p&gt;

&lt;p&gt;The form will open in a modal with content populated dynamically by the server, the server will process the form submission, and the DOM will updated without a full-page turn.&lt;/p&gt;

&lt;p&gt;To accomplish this, we’ll use &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; for the front-end interactivity, &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/&quot;&gt;CableReady’s&lt;/a&gt; brand new &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/cable-car&quot;&gt;CableCar feature&lt;/a&gt; to send content back from the server, and &lt;a href=&quot;https://mrujs.com/&quot;&gt;Mrujs&lt;/a&gt; to enable AJAX requests and to automatically process CableCar’s operations.&lt;/p&gt;

&lt;p&gt;It’ll be pretty fancy.&lt;/p&gt;

&lt;p&gt;When we’re finished, our application will look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-09-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of an initially empty web page with a header that reads Customers and a link to create new customers. The user clicks on the new customer link adn a pop-up modal displays on the screen. The user types in a name, creating a customer and the page updates automatically with the new customer&apos;s inforamtion. The user continues to add and update a few more customer records, each time the form opens in a modal and the page updates with the user&apos;s change immediately.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This article includes a fair amount of JavaScript and assumes a solid understanding of the basics of Ruby on Rails.&lt;/p&gt;

&lt;p&gt;If you’ve never used Rails before, this article might move a little too quickly for you. While comfort with Rails and JavaScript are needed, you don’t need to have any prior experience with CableReady or Stimulus.&lt;/p&gt;

&lt;p&gt;As usual, you can find the complete source code for this article on &lt;a href=&quot;https://github.com/DavidColby/tiny_crm/tree/implement-modals&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s dive in!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;If you prefer to skip the setup steps and jump right in to coding, you can clone the main branch of &lt;a href=&quot;https://github.com/DavidColby/tiny_crm/tree/main&quot;&gt;this repo&lt;/a&gt; and then &lt;a href=&quot;#customers-layout&quot;&gt;scroll down to the Customers Layout&lt;/a&gt; section.&lt;/p&gt;

&lt;p&gt;To get everything installed, we’re going to walk on the wild side by using the newly released, very-much-still-alpha &lt;a href=&quot;https://github.com/rails/jsbundling-rails&quot;&gt;jsbundling-rails&lt;/a&gt; and &lt;a href=&quot;https://github.com/rails/cssbundling-rails&quot;&gt;cssbundling-rails&lt;/a&gt; gems.&lt;/p&gt;

&lt;p&gt;First, we’ll create a Rails application and use the alpha js/cssbundling gems to install Webpack and Tailwind, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new tiny_crm &lt;span class=&quot;nt&quot;&gt;--skip-webpack-install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--skip-javascript&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;tiny_crm
bundle add jsbundling-rails cssbundling-rails
rails javascript:install:webpack
rails css:install:tailwind
bin/dev&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note that these gems are VERY new, if you bump into errors, check the documentation to see if commands have changed or reach out to me and let me know what error you’re encountering.&lt;/p&gt;

&lt;p&gt;With Webpack and Tailwind installed, next we’ll install the core dependencies for this guide, Stimulus, CableReady (plus Action Cable), and Mrujs, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;bundle add hotwire-rails
be rails hotwire:install&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update your Gemfile to pull in the latest cable_ready.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cable_ready&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;github: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;stimulusreflex/cable_ready&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;
&lt;p&gt;Note that if you’re reading this in the future, we’re using 5.0 for this guide.&lt;/p&gt;

&lt;p&gt;And then from your terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;bundle
yarn add mrujs cable_ready @rails/actioncable&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/packs/application.js&lt;/code&gt; like this, to pull in the new dependencies and configure Mrujs to use its &lt;a href=&quot;https://mrujs.com/how-tos/integrate-cablecar&quot;&gt;CableCar plugin&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;./controllers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mrujs&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;mrujs&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;CableReady&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;cable_ready&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;CableCar&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;mrujs/plugins&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Turbo&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/turbo&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;Turbo&lt;/span&gt; &lt;span class=&quot;o&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;mrujs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&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;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CableCar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;CableReady&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;That’s a lot of dependencies to setup. Do we really need all of this to display a modal? No, no not really.&lt;/p&gt;

&lt;p&gt;The techniques we’ll use in this article only require Action Cable (a core Rails library), CableReady, and Mrujs.&lt;/p&gt;

&lt;p&gt;Tailwind and Stimulus are requirements to follow along with the guide step-by-step, but we’re just using them to do things that can be done with your own CSS and vanilla JavaScript, if that’s your preference.&lt;/p&gt;

&lt;p&gt;Ultimately, the only UI component you need is a modal that can open and close. Stimulus and Tailwind are a simple way to get there, but they’re not the only way!&lt;/p&gt;

&lt;p&gt;Moving on, to wrap up the copy/pasting setup work, we’ll be creating and editing Customers in this application, so let’s scaffold up that resource:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails g scaffold Customer name:string
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Setup is complete! Great work so far. Now we can start writing code.&lt;/p&gt;

&lt;h2 id=&quot;customers-layout&quot;&gt;Customers layout&lt;/h2&gt;
&lt;p&gt;First, we’ll apply some basic styling to the customers index page:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;max-w-3xl mx-auto mt-8&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-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modal&quot;&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-baseline mb-6&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;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Customers&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Customer&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_customer_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;text-blue-600&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;click-&amp;gt;modal#open&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;render&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;modal&apos;&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;customers&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 items-baseline space-y-6 p-4 shadow-lg rounded&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;@customers&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;customer&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;render&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;ss&quot;&gt;customer: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;customer&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&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The index view renders a list of customers, plus a header that includes a link to create a new customer.&lt;/p&gt;

&lt;p&gt;The header container div includes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;controller=&quot;modal&quot;&lt;/code&gt; data attribute which is a reference to a Stimulus controller that doesn’t exist yet. Likewise, the new customer link references the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal&lt;/code&gt; controller in its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;We’ll create that controller soon, for now though, clicking on the new customer link will navigate the browser to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customers/new&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The index view also renders two partials that don’t exist yet, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customer&lt;/code&gt;. Let’s create and fill those in next so that we can render the index page again.&lt;/p&gt;

&lt;p&gt;First, the customer partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/customers/_customer.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The customer partial just renders the customer’s name for now:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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-gray-700 border-b border-gray-200 w-full pb-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;n&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next create the modal partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/customers/_modal.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-modal-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;hidden fixed inset-0 overflow-y-auto flex items-center justify-center&quot;&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;z-index: 9999;&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;max-w-lg max-h-screen w-full relative&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;m-1 bg-white rounded shadow&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;px-4 py-5 border-b border-gray-200 sm:px-6&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;h3&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-6 font-medium text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Customer
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/h3&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;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;customer_form&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/form&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The important items here are the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal-target=&quot;container&quot;&lt;/code&gt;  data attribute, which the Stimulus controller will use to open/close the modal and the empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;This form element will eventually be filled in with content from the server when the user opens the modal.&lt;/p&gt;

&lt;p&gt;With the index markup in place and your server running via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/dev&lt;/code&gt;, head to &lt;a href=&quot;http://localhost:3000/customers&quot;&gt;http://localhost:3000/customers&lt;/a&gt; and make sure everything is displaying as expected.&lt;/p&gt;

&lt;p&gt;Next we will create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal&lt;/code&gt; Stimulus controller and fill it in with content rendered from the server. I’m excited too.&lt;/p&gt;

&lt;h2 id=&quot;showing-the-new-customer-modal&quot;&gt;Showing the new customer modal&lt;/h2&gt;
&lt;p&gt;First we need to create a new Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails g stimulus modal&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill the controller in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// Credit: This controller is an edited-to-the-essentials version&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// of the modal component created by @excid3 as part of the essential &lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// tailwind-stimulus-components package found here:&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// https://github.com/excid3/tailwindcss-stimulus-components&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// In production, use the full component from the &lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// library or expand this controller to allow for &lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// keyboard closing and dealing with scroll positions&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;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;nc&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;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;container&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;nf&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;toggleClass&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;hidden&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;modal-background&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundHtml&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;nf&quot;&gt;_backgroundHTML&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;allowBackgroundClose&lt;/span&gt; &lt;span class=&quot;o&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;nf&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;nf&quot;&gt;close&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;open&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;body&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;nf&quot;&gt;add&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;fixed&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;inset-x-0&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;overflow-hidden&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;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;nf&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;toggleClass&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;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;insertAdjacentHTML&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;beforeend&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundHtml&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;background&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;nf&quot;&gt;querySelector&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;backgroundId&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;nf&quot;&gt;close&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;typeof&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;undefined&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;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;preventDefault&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;nf&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;toggleClass&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;background&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;background&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;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;_backgroundHTML&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;`&amp;lt;div id=&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;backgroundId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot; class=&quot;fixed top-0 left-0 w-full h-full&quot; style=&quot;background-color: rgba(0, 0, 0, 0.7); z-index: 9998;&quot;&amp;gt;&amp;lt;/div&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;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There’s a lot of JavaScript here, but it isn’t doing anything too fancy.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;connect&lt;/code&gt;, we set default values the controller needs to function.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;open&lt;/code&gt; simply applies classes to the body and the modal container to make the modal visible on the screen and apply the standard grayed-out background to the rest of the page.&lt;/p&gt;

&lt;p&gt;When &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;close&lt;/code&gt; is called, the background is removed and the modal is hidden.&lt;/p&gt;

&lt;p&gt;If you decide to use this approach in a real project, consider using the &lt;a href=&quot;https://github.com/excid3/tailwindcss-stimulus-components&quot;&gt;Stimulus component&lt;/a&gt; this code is derived from. The above code was edited for brevity and the edits will introduce issues with scrolling and accessibility that the full component handles cleanly.&lt;/p&gt;

&lt;p&gt;With the Stimulus controller created, we’re almost ready to render the modal. Before we proceed, let’s step back and make sure we’re clear on what we want to achieve.&lt;/p&gt;

&lt;p&gt;Our goal is to create a server-rendered modal that allows a user to create a new customer. After the form in the modal is submitted, the newly created customer should be inserted into the list of customers, and the modal should close.&lt;/p&gt;

&lt;p&gt;The first task is to open the modal and display the content from the server, which means that when a user clicks on the New Customer link on the index page:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;A request should be made to the server to retrieve the content for the customer form&lt;/li&gt;
  &lt;li&gt;The content should replace the empty customer form that the modal partial renders on the initial page load&lt;/li&gt;
  &lt;li&gt;The modal should open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will be easier than it sounds.&lt;/p&gt;

&lt;p&gt;We’ll use Mrujs to make an AJAX request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customers#new&lt;/code&gt;, we’ll queue up operations with CableCar, and Mrujs will automatically process those operations for us.&lt;/p&gt;

&lt;p&gt;First we need to tell Mrujs to convert the New Customer link to a CableCar-enabled link.&lt;/p&gt;

&lt;p&gt;As described in &lt;a href=&quot;https://mrujs.com/how-tos/integrate-cablecar&quot;&gt;the documentation&lt;/a&gt;, we’ll do that by updating the link on the index page like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s1&quot;&gt;&apos;New Customer&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_customer_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;text-blue-600&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;click-&amp;gt;modal#open&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;remote: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-remote=&quot;true&quot;&lt;/code&gt; to the link. Once we do this Mrujs &lt;a href=&quot;https://mrujs.com/how-tos/integrate-cablecar#using-cablecar&quot;&gt;takes care of the rest&lt;/a&gt; for us by adding a special cable-ready &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Accept&lt;/code&gt; header when the link is clicked.&lt;/p&gt;

&lt;p&gt;With this change in place, when the user clicks on the New Customer link, a request will be sent to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customers#new&lt;/code&gt; that our controller will (eventually) respond to with Cable Ready operations.&lt;/p&gt;

&lt;p&gt;Since we’re going to be rendering the form partial shortly, let’s go ahead and update that partial now:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &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;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;customer_form&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;px-4 pt-5 pb-4 sm:p-6 sm:pb-4&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;n&quot;&gt;customer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;p-4 border border-red-600&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&amp;gt;&lt;/span&gt;
          Could not save customer
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&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;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_messages&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;message&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;n&quot;&gt;message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&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;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&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&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-b mt-6 px-4 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;w-full sm:col-start-2 bg-blue-600 px-4 py-2 mb-4 text-white rounded-sm hover:bg-blue-700&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;button&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;modal#close&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mt-3 w-full sm:mt-0 sm:col-start-1 mb-4 bg-gray-100 hover:bg-gray-200 rounded-sm px-4 py-2&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;/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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Most of this is standard Tailwind classes to apply some light styling to the form.&lt;/p&gt;

&lt;p&gt;The important pieces are the id of the form, assigned on line 1, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; assigned to the close button, which fires the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;close&lt;/code&gt; function we defined in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal&lt;/code&gt; Stimulus controller earlier.&lt;/p&gt;

&lt;p&gt;We can also make the form look nicer with Tailwind’s form plugin. This is optional, but if you’d like to use it, first install it with yarn, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;yarn add @tailwindcss/forms&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tailwind.config.js&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;mode&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;jit&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;na&quot;&gt;purge&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;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./app/views/**/*.html.erb&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;./app/helpers/**/*.rb&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;./app/javascript/**/*.js&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;na&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;nf&quot;&gt;require&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;@tailwindcss/forms&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next we need to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; method in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomersController&lt;/code&gt; to render &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/reference/operations&quot;&gt;CableReady operations&lt;/a&gt;, using the newly introduced &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/cable-car#ajax-mode&quot;&gt;CableCar&lt;/a&gt;. To do that, we’ll make two changes to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomersController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First update the controller to &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/cableready-everywhere#controller-actions&quot;&gt;include CableReady::Broadcaster&lt;/a&gt; to give the controller access to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_car&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CustomersController&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;CableReady&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Broadcaster&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# snip&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Feel free to place the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;include&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt; if you prefer.&lt;/p&gt;

&lt;p&gt;Then update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomersController&lt;/code&gt; new method as follows:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Customer&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;outer_html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;#customer_form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re rendering the form partial to a string which we then pass to cable_car and use in an &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/reference/operations/dom-mutations#outer_html&quot;&gt;outer_html operation&lt;/a&gt;, targeting the (currently empty) customer form.&lt;/p&gt;

&lt;p&gt;With all this in place, head back to &lt;a href=&quot;http://localhost:3000/customers&quot;&gt;http://localhost:3000/customers&lt;/a&gt; and click on the New Customer link. If all has gone well, you should see the modal open and the customer form render.&lt;/p&gt;

&lt;p&gt;Incredible work so far.&lt;/p&gt;

&lt;p&gt;Next up we’ll use this same CableCar approach to handle form submissions.&lt;/p&gt;

&lt;h2 id=&quot;submitting-the-form&quot;&gt;Submitting the form&lt;/h2&gt;
&lt;p&gt;This section is going to look pretty familiar. We’ll start by updating the customer form with the remote data attribute, just like we added to the new customer link in the last section:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &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;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;customer_form&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;remote: &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;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Again, this tells Mrujs to submit the form with an AJAX request and to expect CableReady operations to perform in response.&lt;/p&gt;

&lt;p&gt;Next, head back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customers_controller.rb&lt;/code&gt; and update the create method:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@customer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Customer&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;customer_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;@customer&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;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;customer&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&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;s1&quot;&gt;&apos;#customers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;c1&quot;&gt;# TODO: Handle errors&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re again rendering a partial to a string and passing that string to an operation (this time, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;append&lt;/code&gt;). The target is the list of the customers rendered in the customers index view, where the newly created customer will be added to the bottom of the list. If you prefer, use &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/operations/dom-mutations#prepend&quot;&gt;prepend&lt;/a&gt; to add the customer to the top of the list instead.&lt;/p&gt;

&lt;p&gt;With this in place, open up the modal, type in a name, and submit the form. You should see the newly created customer get appended to the list like expected but the modal doesn’t close.&lt;/p&gt;

&lt;p&gt;That’s not ideal.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-09-submitting-without-closing.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a web page. The user clicks a link on the page that reads New Customer and a modal opens. The user submits the modal and the customer they created is added to the page but the modal stays open&quot; /&gt;&lt;/p&gt;

&lt;p&gt;How do we close the modal when the form submission is successful? By tapping into some of what makes CableReady so powerful — &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/cableready-101#method-chaining&quot;&gt;chaining operations&lt;/a&gt; and &lt;a href=&quot;https://cableready.stimulusreflex.com/v/v5/reference/operations/event-dispatch&quot;&gt;emitting custom DOM events&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To make this work, we’ll first add another operation to the operations chain sent back to Mrujs:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;c1&quot;&gt;# snip&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&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;s1&quot;&gt;&apos;#customers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;dispatch_event&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;s1&quot;&gt;&apos;submit:success&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt; &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/operations/event-dispatch&quot;&gt;operation&lt;/a&gt; allows us to emit whatever event we like. With this new event dispatched on successful submission, closing the modal is as simple as adding an event listener to the modal’s Stimulus controller, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;open&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;nf&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;submit:success&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;close&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;c1&quot;&gt;// snip&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When the modal opens, an event listener is created, tuned to the event name that is dispatched from the cable_car payload.&lt;/p&gt;

&lt;p&gt;Now when you submit the modal form, both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;append&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatch_event&lt;/code&gt; operations are sent back in response to a successful form submission, Mrujs magic automatically performs the operations, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit:success&lt;/code&gt; event listener closes the modal.&lt;/p&gt;

&lt;h2 id=&quot;handling-errors&quot;&gt;Handling errors&lt;/h2&gt;
&lt;p&gt;Wonderful work stuff so far. Next we’ll deal with form errors using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render operations&lt;/code&gt; again.&lt;/p&gt;

&lt;p&gt;First, make it possible for a customer submission to have a validation error by adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;validates_presence_of :name&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;models/customer.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;With that in place, when the form is submitted with a blank name, the form submission will fail. When that happens, we want to render the customer form inside of the modal, with the validation errors attached.&lt;/p&gt;

&lt;p&gt;To render errors in response to a failed submission, update the create method like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@customer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Customer&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;customer_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;@customer&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;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;customer&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&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;s1&quot;&gt;&apos;#customers&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;dispatch_event&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;s1&quot;&gt;&apos;submit:success&apos;&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;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inner_html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;#customer_form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;else&lt;/code&gt; branch, we again render the partial to a string and render operations. This time, since we don’t want the modal to close and we don’t need to replace the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element itself, we can just use one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inner_html&lt;/code&gt;  operation.&lt;/p&gt;

&lt;p&gt;Open up the modal, submit a blank form, and see that the form is re-rendered with the errors as expected.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-09-submitting-with-errors.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a web page. The user clicks a link on the page that reads New Customer and a modal opens. The user submits the form without typing anything in and the form updates with an error message that name is required&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You’re a star for making it this far. Let’s finish up by seeing how easy it is to reuse this modal for editing customers, and adding some small optimizations to the modal opening.&lt;/p&gt;

&lt;h2 id=&quot;cable-car-customer-edits&quot;&gt;Cable Car customer edits&lt;/h2&gt;
&lt;p&gt;A cool thing about the empty customer form modal is that we can reuse it with no modifications for editing existing customers, leaving us with just one tiny modal container that we can reuse for any number of modals on the page.&lt;/p&gt;

&lt;p&gt;First, add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cable_car&lt;/code&gt; enabled modal link to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customer&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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-gray-700 border-b border-gray-200 w-full pb-2&quot;&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;customer-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&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;id&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;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;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;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;n&quot;&gt;edit_customer_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&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;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;remote: &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;action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;click-&amp;gt;modal#open&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we setup the relevant data attributes on the link and, on the wrapper div, we added a unique id. We’ll use that id to replace the content of the customer when the edit form is submitted.&lt;/p&gt;

&lt;p&gt;Next up, back to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomersController&lt;/code&gt; to adjust the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; methods:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;edit&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&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;s1&quot;&gt;&apos;#customer_form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;update&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&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;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;customer_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;customer&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&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;s2&quot;&gt;&quot;#customer-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&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;id&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;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;dispatch_event&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;s1&quot;&gt;&apos;submit:success&apos;&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;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inner_html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;#customer_form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This should look pretty familiar. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt; method is a mirror of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; method, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt; method is a mirror of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; method. Again, we dispatch the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;submit:success&lt;/code&gt; event when the customer is updated, otherwise the form re-renders with errors.&lt;/p&gt;

&lt;p&gt;Finally, to use the same modal controller for every modal link on the page, we’ll move the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;modal&quot;&lt;/code&gt; declaration one level up the DOM tree. In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;customers/index.html.erb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;max-w-3xl mx-auto mt-8&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;modal&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 justify-between items-baseline mb-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;!-- Snip --&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With these changes in place, refresh the customers index page, click on a customer’s name, and see that updating the customer happens in a modal, and the customer is updated in place in the list on a successful form submission.&lt;/p&gt;

&lt;h2 id=&quot;optimizing-modal-opening&quot;&gt;Optimizing modal opening&lt;/h2&gt;
&lt;p&gt;Something you may have noticed as you’ve worked through this guide is that the modal opens before the content from the server has rendered, causing a very brief flash as the modal opens and then quickly replaces the empty form or the form’s previous contents:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-09-content-flash.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a web page. The user opens and closes a New Customer modal several times. Each time, for a brief moment, the modal displays the content it had the last time it was opened before the content is updated&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This happens because the modal opens instantly when a modal link is clicked but the round trip to the server to retrieve the form partial is not &lt;em&gt;quite&lt;/em&gt; instant.&lt;/p&gt;

&lt;p&gt;We have options for how to prevent this, including adding a loading state to the modal to make the re-render less jarring, but the method I’ll demonstrate is keeping the modal hidden until the content has been retrieved from the server. This gives us another chance to use CableReady and Stimulus, and that’s what we’re all here for, right?&lt;/p&gt;

&lt;p&gt;First, add another event listener to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal&lt;/code&gt; controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;open&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;nf&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;modal:loaded&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;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;nf&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;toggleClass&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;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;submit:success&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;close&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;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&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;nf&quot;&gt;add&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;fixed&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;inset-x-0&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;overflow-hidden&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;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;insertAdjacentHTML&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;beforeend&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;backgroundHtml&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;background&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;nf&quot;&gt;querySelector&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;backgroundId&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we updated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;open&lt;/code&gt; to move the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;containerTarget.classList.remove&lt;/code&gt; call from happening instantly to happening in response to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal:loaded&lt;/code&gt; DOM event.&lt;/p&gt;

&lt;p&gt;This change means that all of the modal links are now broken because the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal:loaded&lt;/code&gt; event never occurs and so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;containerTarget.classList.remove&lt;/code&gt; never runs and the modal container stays hidden.&lt;/p&gt;

&lt;p&gt;We can fix the modal links by updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CustomersController&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Customer&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;outer_html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;#customer_form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;dispatch_event&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;s1&quot;&gt;&apos;modal:loaded&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;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;edit&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;html&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render_to_string&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;customer: &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;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;operations: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cable_car&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;outer_html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;#customer_form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&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;dispatch_event&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;s1&quot;&gt;&apos;modal:loaded&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit&lt;/code&gt; methods, we again take advantage of CableReady’s chainable operations to dispatch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal:loaded&lt;/code&gt; after the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;outer_html&lt;/code&gt; is replaced.&lt;/p&gt;

&lt;p&gt;With this change, the sequence of events when the user clicks on a modal link is:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Request to server begins&lt;/li&gt;
  &lt;li&gt;Open action is triggered&lt;/li&gt;
  &lt;li&gt;Modal backdrop is applied to the page, no visible modal yet&lt;/li&gt;
  &lt;li&gt;Form content is replaced&lt;/li&gt;
  &lt;li&gt;Modal loaded event is dispatched&lt;/li&gt;
  &lt;li&gt;Hidden class is removed from the modal, making it visible&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sequence happens rapidly enough in our circumstances for the user to barely notice the delay between the backdrop being applied and the modal displaying. In a production environment, you may find that a loading state for an immediately-opened modal is a more scalable option, but we’re here to learn about CableReady and Mrujs, not build a production application.&lt;/p&gt;

&lt;p&gt;With these changes in place, the modal will open with the updated content already populated, eliminating the flash of old content.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-09-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of an initially empty web page with a header that reads Customers and a link to create new customers. The user clicks on the new customer link adn a pop-up modal displays on the screen. The user types in a name, creating a customer and the page updates automatically with the new customer&apos;s inforamtion. The user continues to add and update a few more customer records, each time the form opens in a modal and the page updates with the user&apos;s change immediately.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modal&lt;/code&gt; connected div can be used to display any number of modals, serving as a way to reduce the initial page load in a more traditional application which might pre-render each edit modal.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we learned how to build a server rendered modal form, powered by Stimulus, CableReady, and Mrujs.&lt;/p&gt;

&lt;p&gt;Stimulus and CableReady are two powerful, battle-tested tools with a mature feature set that should be considered for any modern Rails application. CableReady can stand alone as a way to deliver real-time updates to end users through a variety of methods or it can be powered-up with &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;StimulusReflex&lt;/a&gt; to deliver a SPA-link experience, minus the SPA.&lt;/p&gt;

&lt;p&gt;Mrujs is a newer tool, under active development, and is intended to serve as a modern, stable replacement for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails/ujs&lt;/code&gt;, which is no longer under active development and which will be &lt;a href=&quot;https://github.com/rails/rails/pull/43112&quot;&gt;deprecated&lt;/a&gt; when Rails 7 releases.&lt;/p&gt;

&lt;p&gt;In addition to the tight integration with CableReady’s Cable Car that we saw today, Mrujs gives &lt;a href=&quot;https://mrujs.com/tutorials/practical-guide-to-mrujs&quot;&gt;you access&lt;/a&gt; to simple confirmation dialogs, disabled links, and the other niceties from Rails UJS, in a modern package.&lt;/p&gt;

&lt;p&gt;An important note before we go: we could build a very similar user experience with a variety of tools in the Rails ecosystem, including Turbo Streams (&lt;a href=&quot;https://www.colby.so/posts/handling-modal-forms-with-rails-and-hotwire&quot;&gt;here’s a guide&lt;/a&gt; for that).&lt;/p&gt;

&lt;p&gt;While the full &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire stack&lt;/a&gt; can deliver this experience with about the same amount of effort, the power and flexibility of CableReady’s chainable operations makes CableReady + Mrujs a better fit for this particular use case than the full Hotwire stack, in my very, very humble opinion.&lt;/p&gt;

&lt;p&gt;What’s really exciting about this is that as Rails developers, our cups are overflowing with powerful tools to build real-time, reactive applications. That means we all win, no matter which tool we reach for most often.&lt;/p&gt;

&lt;p&gt;Continue your journey with CableReady, Stimulus, and Mrujs with these resources:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The &lt;a href=&quot;https://cableready.stimulusreflex.com/&quot;&gt;CableReady documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://stimulus.hotwired.dev/reference/controllers&quot;&gt;Stimulus docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://mrujs.com/tutorials/getting-started&quot;&gt;Mrujs docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Explore the &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;StimulusReflex documentation&lt;/a&gt; when you’re ready&lt;/li&gt;
  &lt;li&gt;Join the &lt;a href=&quot;https://discord.gg/stimulus-reflex&quot;&gt;StimulusReflex discord&lt;/a&gt; if you get stuck with CableReady or StimulusReflex&lt;/li&gt;
  &lt;li&gt;(Shameless plug) Subscribe to my monthly newsletter, &lt;a href=&quot;https://landing.mailerlite.com/webforms/landing/d7z0n0&quot;&gt;Hotwiring Rails&lt;/a&gt;, to stay up to date on the latest on building modern, performant applications with Rails and tools like CableReady and Stimulus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all for today.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Doing the Impossible — Building a Persistent Audio Player in Ruby on Rails</title>
   <link href="https://colby.so/posts/doing-the-impossible-persistent-audio-player-in-rails"/>
   <updated>2021-09-05T00:00:00+00:00</updated>
   <id>https://colby.so/posts/doing-the-impossible-persistent-audio-player-in-rails</id>
   <content type="html">&lt;p&gt;Today we’re going to learn how to build a Ruby on Rails app that accomplishes what some folks think is &lt;a href=&quot;https://mobile.twitter.com/Rich_Harris/status/1433057805477552132&quot;&gt;impossible&lt;/a&gt; in a multi-page application — persisting an audio player while navigating around an application.&lt;/p&gt;

&lt;p&gt;We’ll use Ruby on Rails and &lt;a href=&quot;https://turbo.hotwired.dev/&quot;&gt;Turbo&lt;/a&gt; to accomplish this, but we could use Turbo’s predecessor, &lt;a href=&quot;https://github.com/turbolinks/turbolinks&quot;&gt;Turbolinks&lt;/a&gt; to achieve the same result, and Rails is only incidental to the finished project. We could just as easily use Turbo with any other “multi-page” framework and deliver the same experience.&lt;/p&gt;

&lt;p&gt;When we’re finished, our application will allow users to create and manage Ideas along with a persistent audio player tuned to a white noise internet radio station, to help them focus while they generate ideas.&lt;/p&gt;

&lt;p&gt;Users will be able to start and stop the audio using a standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; input. When audio is playing, it will continue playing as the user navigates around the application.&lt;/p&gt;

&lt;p&gt;The application will look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-05-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a web page. On the web page is an audio player and interface to create and view Ideas. The user clicks play on the audio player and then navigates through pages of the idea management interface. While they navigate, the audio player continues playing.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After we’ve built our application, we’ll spend a time talking about myths in web development, and how to avoid falling into the expert-led myth-trap.&lt;/p&gt;

&lt;p&gt;This article assumes basic familiarity with Ruby on Rails, but no prior experience with Turbo is required, and you should be able to follow along even if you’ve never seen Rails code before.&lt;/p&gt;

&lt;p&gt;Let’s get started.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application Setup&lt;/h2&gt;
&lt;p&gt;To begin, we’ll create a standard Ruby on Rails app, install Turbo, and scaffold an Ideas resource that we’ll use to demonstrate the “multi-page” part of the application.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new rails_focused_ideas
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;rails_focused_ideas
bundle add turbo-rails
rails turbo:install
rails g scaffold Idea title:string description:text
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To finish installing Turbo, open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/packs/application.js&lt;/code&gt; and update it like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Rails&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/ujs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/turbo-rails&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ActiveStorage&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/activestorage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;channels&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;ActiveStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve removed any references to turbolinks and added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import &quot;@hotwired/turbo-rails&quot;&lt;/code&gt; line to use Turbo throughout our application.&lt;/p&gt;

&lt;p&gt;When you’ve finished setting up the application, start your rails server (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails s&lt;/code&gt; from your terminal) and navigate to &lt;a href=&quot;http://localhost:3000/ideas&quot;&gt;http://localhost:3000/ideas&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;add-audio-player-to-layout&quot;&gt;Add audio player to layout&lt;/h2&gt;
&lt;p&gt;Next up, we’ll add an audio element to our application layout (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt;), just after the opening &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;audio&lt;/span&gt; 
  &lt;span class=&quot;na&quot;&gt;controls&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://uk1.internet-radio.com:8267/listen.pls&amp;amp;t=.m3u&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      Your browser does not support the
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;code&amp;gt;&lt;/span&gt;audio&lt;span class=&quot;nt&quot;&gt;&amp;lt;/code&amp;gt;&lt;/span&gt; element.
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/audio&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute in this code is a random white noise internet station and it could go down tomorrow or ten years from now. You’re free to use whatever you like for the source, including a local file accessible to your Rails server or any other &lt;a href=&quot;https://www.internet-radio.com/&quot;&gt;internet radio station&lt;/a&gt; you like.&lt;/p&gt;

&lt;p&gt;Now if we refresh the page, we’ll see our audio element is there and we can start and stop it, but every time we navigate to a new page, the audio stops playing.&lt;/p&gt;

&lt;p&gt;How do we make it persist?&lt;/p&gt;

&lt;h2 id=&quot;persisting-the-audio-player-across-page-turns&quot;&gt;Persisting the audio player across page turns&lt;/h2&gt;
&lt;p&gt;To make the audio persist across page turns, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element like this:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;audio&lt;/span&gt; 
  &lt;span class=&quot;na&quot;&gt;data-turbo-permanent&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;white-noise-player&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;controls&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;http://uk1.internet-radio.com:8267/listen.pls&amp;amp;t=.m3u&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    Your browser does not support the
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;code&amp;gt;&lt;/span&gt;audio&lt;span class=&quot;nt&quot;&gt;&amp;lt;/code&amp;gt;&lt;/span&gt; element.
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/audio&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We’ve done the impossible and made our audio element persistent across page turns by making two changes.&lt;/p&gt;

&lt;p&gt;First, we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-permanent&lt;/code&gt; to the element to tell Turbo that this element should &lt;a href=&quot;https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads&quot;&gt;persisted across page loads&lt;/a&gt;. Next, we added an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; to the audio element, which Turbo uses to match the element when it renders pages.&lt;/p&gt;

&lt;p&gt;With these small changes in place, we can refresh the page, start up the audio player, and then navigate around our application, clicking links and navigating through the browser history to our hearts content without ever stopping the audio player.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-05-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a web page. On the web page is an audio player and interface to create and view Ideas. The user clicks play on the audio player and then navigates through pages of the idea management interface. While they navigate, the audio player continues playing.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Magical.&lt;/p&gt;

&lt;h2 id=&quot;why-write-this-article&quot;&gt;Why write this article?&lt;/h2&gt;
&lt;p&gt;The problem we solved today — building a persistent audio player with Turbo(links) — has been &lt;a href=&quot;https://changelog.com/posts/why-we-chose-turbolinks&quot;&gt;solved&lt;/a&gt; for a long time.&lt;/p&gt;

&lt;p&gt;Why take the time to write up something that’s so simple?&lt;/p&gt;

&lt;p&gt;Because, as we saw in the tweet linked in the introduction, dangerous misconceptions exist in web development. These misconceptions are often spread by folks who want you to believe that their way is the One True Path.&lt;/p&gt;

&lt;p&gt;Downstream of this bad information are developers researching problems who absorb the bad information and trust it because it comes from someone who has such a big platform that they must know what they’re talking about.&lt;/p&gt;

&lt;p&gt;Some of those developers will then make decisions based on the bad information and justify their decisions in conversations with other developers by repeating the bad information until it is eventually picked up and presented as truth by a new flock of experts.&lt;/p&gt;

&lt;p&gt;The bad information continues flowing, poisoning the conversation for years to come, turning into myths based on half-truths, misunderstandings, and overconfidence.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-05-myth-cycle.png&quot; class=&quot;centered&quot; alt=&quot;An image of two circles, connected by arrows. The first circle reads &apos;Experts share bad information&apos; and the second circle reads &apos;Bad info spreads through the community&apos;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is how myths like “Rails doesn’t scale” propagate and persist, when even a cursory amount of research disproves the idea.&lt;/p&gt;

&lt;h2 id=&quot;defending-against-myths&quot;&gt;Defending against myths&lt;/h2&gt;
&lt;p&gt;You can avoid falling for these myths by remembering that everyone writing about web development on the internet only knows a tiny fraction of all that there is to know about web development.&lt;/p&gt;

&lt;p&gt;No one is an expert on every language and every method for building web applications. Instead, all of us, at best, know a lot about a few things, and a little about a lot more things.&lt;/p&gt;

&lt;p&gt;We make decisions and recommendations based on that very limited knowledge, which means we can never confidently speak in absolutes. We can only share what we think is best, based on our own knowledge, for better or worse.&lt;/p&gt;

&lt;p&gt;There is no One True Path in web development. Many different valid solutions exist, each with their own set of benefits and tradeoffs.&lt;/p&gt;

&lt;p&gt;Fortunately, we don’t need One True Path. Many paths can lead to the same destination.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-09-05-framework.png&quot; class=&quot;centered&quot; alt=&quot;A venn diagram with two sides that don&apos;t overlap. One side reads &apos;The framework you choose&apos; and the other reads &apos;Things that make your product successful&apos;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;With this in mind, try to follow these rules to be a good consumer when you’re exposed to web development content:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Discard any absolute opinion about one technology or framework being “the best” way to solve any particular problem&lt;/li&gt;
  &lt;li&gt;Don’t listen to people who frequently speak in absolutes about languages or frameworks without contextualizing their opinion&lt;/li&gt;
  &lt;li&gt;When comparing languages or frameworks, anyone who says “[x] can’t be used for [y] you have to use [z] instead” is probably just trying to sell you on [z]&lt;/li&gt;
  &lt;li&gt;If someone refuses to acknowledge the weaknesses of their chosen approach, assume they’re not being honest&lt;/li&gt;
  &lt;li&gt;When someone is an expert on a tool, you can probably trust what they’re saying about that specific tool&lt;/li&gt;
  &lt;li&gt;If you don’t like a particular tool or framework there are probably a dozen other tools to solve the same problem that you can use instead&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;wait-arent-you-presenting-yourself-as-an-expert-why-should-i-listen-to-you&quot;&gt;Wait… aren’t you presenting yourself as an expert? Why should I listen to you?&lt;/h2&gt;
&lt;p&gt;Fair. Since I am 1) on the internet and 2) talking about web development, I should share what my particular motivations and interests are so you can have that information as you consider this article.&lt;/p&gt;

&lt;p&gt;I don’t have a big platform (~150 followers on &lt;a href=&quot;https://dev.to/davidcolbyatx&quot;&gt;dev.to&lt;/a&gt; and the same on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;, and ~2,000 people per month reading &lt;a href=&quot;https://colby.so&quot;&gt;my blog&lt;/a&gt;).  I write weekly about Ruby on Rails and the broader Rails ecosystem to a very small audience.&lt;/p&gt;

&lt;p&gt;My writing is mostly aimed at helping newer Rails developers learn how to solve common UX problems that they’ll encounter as they work, and to share some of the modern tools available in the broader Rails ecosystem that enable performant, modern user experiences without needing to turn to a SPA.&lt;/p&gt;

&lt;p&gt;My opinions on Rails are built from ~a decade of working on large production Rails applications and I consider myself to be Pretty Good at building Rails-powered web applications.&lt;/p&gt;

&lt;p&gt;I encourage folks to consider Rails as an alternative to SPAs and JavaScript-everything not because I think JavaScript frameworks are bad, but because I think that Ruby and Rails are wonderful tools, and I’d like more developers to experience them.&lt;/p&gt;

&lt;p&gt;That’s me — now you can decide whether to throw my thoughts on everything in the trash or not.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we built a persistent audio player inside of a simple, multi-page application — from one perspective, an impossible task, from another, very simple.&lt;/p&gt;

&lt;p&gt;We used this impossible/simple problem to talk a little about the myths of web development, and how experts with large platforms can spread those myths (often unintentionally!) to their audience. These myths then propagate through the community at large, and cause unnecessary, impossible to resolve arguments for years to come.&lt;/p&gt;

&lt;p&gt;While we can’t stop experts with large platforms from sharing bad information, as developers, we can be informed about how we consume content. We can be more thoughtful about how we approach learning and growing when we know that absolutes rarely exist and objectively correct answers about which language or framework to use are few and far between in the world of web development.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a Live Search Experience with StimulusReflex and Ruby on Rails</title>
   <link href="https://colby.so/posts/live-search-with-rails-and-stimulusreflex"/>
   <updated>2021-08-28T00:00:00+00:00</updated>
   <id>https://colby.so/posts/live-search-with-rails-and-stimulusreflex</id>
   <content type="html">&lt;p&gt;As we approach the release of Rails 7, the Rails ecosystem is full of options to build modern web applications, fast. Over the last 9 months, I’ve written articles on building type-as-you-search interfaces with &lt;a href=&quot;https://www.colby.so/posts/live-search-with-stimulus-and-rails-6&quot;&gt;Stimulus&lt;/a&gt; and with &lt;a href=&quot;https://www.colby.so/posts/instant-search-with-rails-6-and-hotwire&quot;&gt;the full Hotwire stack&lt;/a&gt;, exploring a few of the options available to Rails developers.&lt;/p&gt;

&lt;p&gt;Today, we’re going to build a live search experience once  more. This time with StimulusReflex, a “new way to craft modern, reactive web interface with Ruby on Rails”. StimulusReflex relies on WebSockets to pass events from the browser to Rails, and back again, and uses &lt;a href=&quot;https://github.com/patrick-steele-idem/morphdom&quot;&gt;morphdom&lt;/a&gt; to make efficient updates on the client-side.&lt;/p&gt;

&lt;p&gt;When we’re finished, our application will look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-08-28-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page with a list of player names below a search text box. The user types &apos;Dirk&apos; in the search box and the list of player names is automatically filtered based on the search term. The user clicks a clear search link and the list of players resets while the text box is cleared.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It won’t win any beauty contests, but it will give us a chance to explore a few of the core concepts of StimulusReflex.&lt;/p&gt;

&lt;p&gt;As we work, you will notice some conceptual similarities between StimulusReflex and Turbo Streams, but there are major differences between the two projects, and StimulusReflex brings functionality and options that don’t exist in Turbo.&lt;/p&gt;

&lt;p&gt;Before we get started, this article will be most useful for folks who are comfortable with Ruby on Rails and who are new to StimulusReflex. If you prefer to skip ahead to the source for the finished project, you can find the full code that accompanies this article on &lt;a href=&quot;https://github.com/DavidColby/stimulus-reflex-search&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s dive in.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;To get started, we’ll create a new Rails application, install StimulusReflex, and scaffold up a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Player&lt;/code&gt; resource that users will be able to search.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new stimulus-reflex-search &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;stimulus-reflex-search
bundle add stimulus_reflex
bundle &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;rails stimulus_reflex:install
rails g scaffold Players name:string
rails db:migrate&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In addition to the above, you’ll also need to have Redis installed and running in your development environment.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulus_reflex:install&lt;/code&gt; task will be enough to get things working in development but you should review the &lt;a href=&quot;https://docs.stimulusreflex.com/hello-world/setup&quot;&gt;installation documentation&lt;/a&gt; in detail ahead of any production deployment of a StimulusReflex application.&lt;/p&gt;

&lt;p&gt;With the core of the application ready to go, start up your rails server and head to &lt;a href=&quot;http://localhost:3000/players&quot;&gt;http://localhost:3000/players&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create a few players in the UI or from the Rails console before moving on.&lt;/p&gt;

&lt;h2 id=&quot;search-with-just-rails&quot;&gt;Search, with just Rails&lt;/h2&gt;
&lt;p&gt;We’ll start by adding the ability to search players without any StimulusReflex at all, just a normal search form that hits the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players#index&lt;/code&gt; action.&lt;/p&gt;

&lt;p&gt;To start, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players/index.html.erb&lt;/code&gt; as shown below:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;notice&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;notice&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;h1&amp;gt;&lt;/span&gt;Players&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;n&quot;&gt;form_with&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;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search by name:&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&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;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Search&apos;&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;kp&quot;&gt;nil&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;table&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&amp;gt;&lt;/span&gt;Name&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;colspan=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;3&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;players: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;br&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;s1&quot;&gt;&apos;New Player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re rendering a search form at the top of the page and we’ve moved rendering players to a partial that doesn’t exist yet.&lt;/p&gt;

&lt;p&gt;Create that partial next:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/_players.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill it in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;tbody&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;players&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;player&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&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;player&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;/td&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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Show&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player&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;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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Edit&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_player_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;player&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;/td&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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Destroy&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player&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;confirm: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;Are you sure?&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;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;span class=&quot;nt&quot;&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we’ll add a very rudimentary search implementation to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; method in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt;, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name LIKE ?&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If you refresh /players now, you should be able to type in a search term, submit the search request to the server, and see the search applied when the index page reloads.&lt;/p&gt;

&lt;p&gt;Now let’s start adding StimulusReflex, one layer at a time.&lt;/p&gt;

&lt;h2 id=&quot;creating-a-reflex&quot;&gt;Creating a reflex&lt;/h2&gt;
&lt;p&gt;StimulusReflex is built around the concept of reflexes. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reflex&lt;/code&gt;, to oversimplify it a bit, is a Ruby class that responds to user interactions from the front end.&lt;/p&gt;

&lt;p&gt;When you’re working with StimulusReflex, you’ll write a lot of reflexes that do work that might otherwise require controller actions and a lot of client-side JavaScript logic.&lt;/p&gt;

&lt;p&gt;Reflexes can do a lot, from re-rendering an entire page on demand to kicking off background jobs, but for our purposes we’re going to create one reflex that handles user interactions with the search form we added in the last section.&lt;/p&gt;

&lt;p&gt;Instead of a GET request to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players#index&lt;/code&gt; action, form submissions will call a method in the reflex class that processes the search request and updates the DOM with the results of the search.&lt;/p&gt;

&lt;p&gt;We’ll start with generating the reflex using the built-in generator:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails g stimulus_reflex PlayerSearch&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This generator creates two files, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; reflex class in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reflexes&lt;/code&gt; directory and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player-search&lt;/code&gt; Stimulus controller in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javascripts/controllers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We’ll define the reflex in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; class, and then, optionally, we can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player-search&lt;/code&gt; controller to trigger that reflex from a front end action and hook into reflex lifecycle methods that we might care about on the front end.&lt;/p&gt;

&lt;p&gt;The simplest implementation of a working &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; reflex is to update an instance variable from the reflex, and then rely on a bit of StimulusReflex magic to do everything else. We’ll start with the magical version.&lt;/p&gt;

&lt;p&gt;First, add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; method to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; reflex:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt;
  &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name LIKE ?&apos;&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update the search form to trigger the reflex on submit:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;method: :get&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;reflex: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;submit-&amp;gt;PlayerSearch#search&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search by name:&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&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;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Search&apos;&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;kp&quot;&gt;nil&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController#index&lt;/code&gt; to only assign a new value to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@players&lt;/code&gt; when it hasn’t already been set by the reflex action.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With these changes in place, we can refresh the players page, submit a search, and see that searching works fine. So what’s going on here?&lt;/p&gt;

&lt;p&gt;In the form, we’re listening for the submit event and, when it is triggered, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-reflex&lt;/code&gt; attribute fires the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; method that we defined in the  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; reflex class.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch.search&lt;/code&gt; automatically gets access the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; from the nearest form so we can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params[:query]&lt;/code&gt; like we would in a controller action.&lt;/p&gt;

&lt;p&gt;We use the query param to assign a value to  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@players&lt;/code&gt; and, because we haven’t told it to do anything else, the reflex finishes by processing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController#index&lt;/code&gt;, passing the updated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; instance variable along the way and using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morphdom&lt;/code&gt; to update the content of the page as efficiently as possible.&lt;/p&gt;

&lt;p&gt;So we can finish this article by deleting the form’s submit button and moving the reflex from the submit event on the form to the input event on the text field, right?&lt;/p&gt;

&lt;p&gt;Not so fast.&lt;/p&gt;

&lt;p&gt;While what we have “works”, our implementation is currently inefficient and hard to maintain and expand. Future developers will have to piece together what’s going on in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt;. We’re also re-rendering the entire HTML body even though we know that only a small part of the page actually needs to change.&lt;/p&gt;

&lt;p&gt;We can do a little better.&lt;/p&gt;

&lt;h2 id=&quot;using-selector-morphs&quot;&gt;Using Selector Morphs&lt;/h2&gt;
&lt;p&gt;The magical re-processing of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action happens because the &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/morph-modes#page-morphs&quot;&gt;default behavior&lt;/a&gt; of a reflex is to trigger a full-page morph when a reflex method runs.&lt;/p&gt;

&lt;p&gt;While page morphs are easy to work with, we can be more explicit about our intentions and more precise in our updates by using &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/morph-modes#selector-morphs&quot;&gt;selector morphs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Selector morphs are more efficient than page morphs because selector morphs skip routing, controller actions, and template rendering. Selector morphs are also more clear in their intention and easier to reason about since we know exactly what will change on the page when the reflex runs.&lt;/p&gt;

&lt;p&gt;Full page morphs are powerful and simple to use, but my preference is to use selector morphs when the use case calls for updating small portions of the page.&lt;/p&gt;

&lt;p&gt;Let’s replace the magical page morph with a selector morph.&lt;/p&gt;

&lt;p&gt;First, as you might have guessed, selector morphs use an identifier to target their DOM changes. We’ll add an id to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;tbody&amp;gt;&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial to give the selector morph something to target.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;tbody&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players-list&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;!-- Snip --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/tbody&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next we’ll update the search form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&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;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search by name:&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&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;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;player-search&quot;&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;input-&amp;gt;player-search#search&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;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve scrapped the submit button and we’ve replaced the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-reflex&lt;/code&gt; on the submit button with a Stimulus controller directly on the query text field.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player-search&lt;/code&gt; controller was created by the generator we ran earlier to create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; reflex, and we’ll fill in the Stimulus controller next:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&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;./application_controller&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;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ApplicationController&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;search&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;nf&quot;&gt;stimulate&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;PlayerSearch#search&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re inheriting from a Stimulus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;, which was automatically created by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulus_reflex:install&lt;/code&gt; task we ran at the beginning of this article. Since we’re inheriting from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;, we have access to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.stimulate&lt;/code&gt;, which we can use to trigger any reflex we like.&lt;/p&gt;

&lt;p&gt;Why would we use a Stimulus controller instead of a  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-reflex&lt;/code&gt; attribute on a DOM element?&lt;/p&gt;

&lt;p&gt;Using a Stimulus controller gives us a little more flexibility and power than if we attach the reflex to the DOM directly, which we’ll explore in the next section.&lt;/p&gt;

&lt;p&gt;Before we expand the Stimulus controller, let’s finish up the implementation of the selector morph by updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch#search&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name LIKE ?&apos;&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#players-list&apos;&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players/players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we no longer need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; to be an instance variable. Instead, we pass it in as a local to the players partial which the selector morph renders to replace the children of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#players-list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With this in place, we can refresh the page and start typing in the search form. If you’ve followed along so far, you should see that as you type, the content of the players table is updated.&lt;/p&gt;

&lt;p&gt;If you check the server logs, you’ll see that instead of the controller action processing and the entire application layout re-rendering, the server only runs the database query to filter the players and then renders the players partial. Skipping routing and full page rendering dramatically reduces the amount of time and resources used to handle the request.&lt;/p&gt;

&lt;h2 id=&quot;expanding-the-stimulus-controller&quot;&gt;Expanding the Stimulus controller&lt;/h2&gt;
&lt;p&gt;Now we’ve got live search in place using a selector morph. Incredible work so far!&lt;/p&gt;

&lt;p&gt;Let’s finish up by expanding the Stimulus controller to make the user experience a bit cleaner and learn a little more about StimulusReflex in the process.&lt;/p&gt;

&lt;p&gt;First, searching on each keystroke isn’t ideal. Let’s adjust &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; to wait for the user to stop typing before calling the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; reflex.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;search&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;clearTimeout&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;timeout&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;timeout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTimeout&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;nf&quot;&gt;stimulate&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;PlayerSearch#search&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Nothing fancy here, and you should probably consider a more battle tested debounce function in production, but it’ll do for today.&lt;/p&gt;

&lt;p&gt;Next, it would be nice to give the user a visual cue that the list of players has updated. One way to do that is to animate the list when it updates and StimulusReflex helpfully gives us an easy way to listen for and react to reflex life-cycle events.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;beforeSearch&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;playerList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;animate&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;fadeIn&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;fadeInTiming&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;nf&quot;&gt;fadeIn&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;opacity&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;na&quot;&gt;opacity&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;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;nf&quot;&gt;fadeInTiming&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;na&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;300&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;nf&quot;&gt;playerList&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;nf&quot;&gt;getElementById&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;players-list&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re combining a custom StimulusReflex &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/lifecycle#custom-life-cycle-methods&quot;&gt;client-side life-cycle&lt;/a&gt; callback (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;beforeSearch&lt;/code&gt;) with the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API&quot;&gt;Web Animations API&lt;/a&gt; to add a simple fade effect to the players list each time it updates.&lt;/p&gt;

&lt;p&gt;In addition to the client-side events, StimulusReflex provides &lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/lifecycle#server-side-reflex-callbacks&quot;&gt;server-side life-cycle callbacks&lt;/a&gt;, which we don’t have a use for in this particular article, but they exist if you need them.&lt;/p&gt;

&lt;p&gt;Now we have visual feedback for users as they type. Let’s finish this article by allowing users to clear a search without having to backspace the input until its empty.&lt;/p&gt;

&lt;p&gt;This last exercise will give us a chance to look at using more than one selector morph in a single reflex and to expand the Stimulus controller a bit more.&lt;/p&gt;

&lt;h2 id=&quot;resetting-search-results&quot;&gt;Resetting search results&lt;/h2&gt;
&lt;p&gt;Our goal is to add a link to the page that displays whenever the search text box isn’t empty. When a user clicks the link, the search box should be cleared, the players list should be updated to list all of the players in the database, and the reset link should be hidden.&lt;/p&gt;

&lt;p&gt;We’ll start by adding a new partial to render the link:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/_reset_link.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;query&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;cp&quot;&gt;%&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;#&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;player-search#reset&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Clear search&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The reset link will only display if the local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query&lt;/code&gt; variable is present. Clicks on the link are routed to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player-search&lt;/code&gt; Stimulus controller, calling the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset&lt;/code&gt; function that doesn’t exist yet.&lt;/p&gt;

&lt;p&gt;Before we update the Stimulus controller, let’s adjust the index view, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;notice&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;notice&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;h1&amp;gt;&lt;/span&gt;Players&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;player-search&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;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;method: :get&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;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search by name:&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&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;input-&amp;gt;player-search#search&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;autocomplete: &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;value: &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;:query&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;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;reset-link&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;reset_link&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;query: &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;:query&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;table&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&amp;gt;&lt;/span&gt;Name&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;colspan=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;3&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;players: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;br&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;s1&quot;&gt;&apos;New Player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve inserted the a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset_link&lt;/code&gt; partial, wrapped in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#reset-link&lt;/code&gt; div.&lt;/p&gt;

&lt;p&gt;More importantly, we’ve adjusted how the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player-search&lt;/code&gt; Stimulus controller is connected to the DOM. Instead of the controller being attached to the search text field, the controller is now on a wrapper div.&lt;/p&gt;

&lt;p&gt;While we didn’t have to make this change to the controller connection, doing so makes it clear that the controller is interested in more than just the text input and opens up the possibility of using targets to more specifically reference DOM elements in the future.&lt;/p&gt;

&lt;p&gt;This change also gives us an opportunity to look at one more piece of functionality of StimulusReflex-enabled functionality in Stimulus controllers.&lt;/p&gt;

&lt;p&gt;Update the Stimulus controller like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&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;clearTimeout&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;timeout&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;timeout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTimeout&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;nf&quot;&gt;stimulate&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;PlayerSearch#search&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;nx&quot;&gt;event&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;value&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;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;nf&quot;&gt;reset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&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;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;preventDefault&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;nf&quot;&gt;stimulate&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;PlayerSearch#search&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We’ve made two important changes here.&lt;/p&gt;

&lt;p&gt;First, since the Stimulus controller is no longer inside of the search form, the search reflex will no longer be able to reference &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; implicitly. We handle this change by passing the value of the search box to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulate&lt;/code&gt; as an additional argument. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Stimulate&lt;/code&gt; “&lt;a href=&quot;https://docs.stimulusreflex.com/rtfm/reflexes#stimulate-is-extremely-flexible&quot;&gt;is extremely flexible&lt;/a&gt;” and we take advantage of that flexibility to ensure the search reflex receives the search query even without access to the search form’s params.&lt;/p&gt;

&lt;p&gt;Next, we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset&lt;/code&gt;, which simply triggers the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; reflex without an additional argument.&lt;/p&gt;

&lt;p&gt;On the server side, we need to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch#search&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&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;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name LIKE ?&apos;&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;query&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;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#players-list&apos;&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players/players&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;players&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;morph&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;#reset-link&apos;&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players/reset_link&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;query: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we updated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; to take an optional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query&lt;/code&gt; argument. The value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;query&lt;/code&gt; is used to set the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; and then two selector morphs replace the content of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players-list&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset-link&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In action, our final product looks like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-08-28-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a web page with a list of player names below a search text box. The user types &apos;Dirk&apos; in the search box and the list of player names is automatically filtered based on the search term. The user clicks a clear search link and the list of players resets while the text box is cleared.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;an-alternative-approach&quot;&gt;An alternative approach&lt;/h2&gt;
&lt;p&gt;If you review the method signature of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimuluate&lt;/code&gt;, you’ll notice that we could have solved the problem of passing the value of the search box to the server in other ways.&lt;/p&gt;

&lt;p&gt;Instead of passing in event.target.value, we could have passed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.target&lt;/code&gt; like this: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this.stimulate(‘PlayerSearch#search, event.target)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This approach would override the default value of the server-side Reflex &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt;, allowing us to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.target.value&lt;/code&gt; to access the value of the search box from the server.&lt;/p&gt;

&lt;p&gt;While this would work for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; function, it wouldn’t work for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset&lt;/code&gt; since we need to ignore the value of the search box when resetting the form. We could make it all work by passing an element to override the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt; assignment, but it would take more effort.&lt;/p&gt;

&lt;p&gt;Passing in the value explicitly allows us to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch#search&lt;/code&gt; to handle both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset&lt;/code&gt; requests and keeps our code a bit cleaner on the server side.&lt;/p&gt;

&lt;p&gt;This is a matter of preference without a definitive answer on which approach is “best”. Implementing a solution overriding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt; on the server side would work fine. Also viable would be using an entirely different reflex action for the reset link.&lt;/p&gt;

&lt;p&gt;StimulusReflex offers plenty of flexibility, and some choices will come down to what feels best to you and your team.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we looked at implementing a simple search-as-you-type interface with Ruby on Rails and StimulusReflex. This simple example should give you some indication of the power StimulusReflex has to deliver modern, fast web applications while keeping code complexity low and developer happiness high.&lt;/p&gt;

&lt;p&gt;Even better, StimulusReflex plays nicely with Turbo Drive and Turbo Frames, giving developers the ability to mix-and-match to choose the best tool for the job.&lt;/p&gt;

&lt;p&gt;To keep learning about building Rails applications with StimulusReflex:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Dive into the (excellent, very well-maintained) &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;official documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Check out &lt;a href=&quot;http://expo.stimulusreflex.com/&quot;&gt;demo applications&lt;/a&gt; demonstrating some core StimulusReflex concepts&lt;/li&gt;
  &lt;li&gt;Join the &lt;a href=&quot;https://discord.gg/stimulus-reflex&quot;&gt;StimulusReflex discord&lt;/a&gt; to learn from lots of folks way sharper than me&lt;/li&gt;
  &lt;li&gt;Learn more advanced usage patterns with &lt;a href=&quot;https://www.stimulusreflexpatterns.com/patterns/&quot;&gt;StimulusReflexPatterns&lt;/a&gt; from &lt;a href=&quot;https://twitter.com/julian_rubisch&quot;&gt;Julian Rubisch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a Real Time Scoreboard with Ruby on Rails and CableReady</title>
   <link href="https://colby.so/posts/building-a-real-time-scoreboard-with-rails-and-cableready"/>
   <updated>2021-08-18T00:00:00+00:00</updated>
   <id>https://colby.so/posts/building-a-real-time-scoreboard-with-rails-and-cableready</id>
   <content type="html">&lt;p&gt;The release of Hotwire in late 2020 brought attention to a growing interest within the Rails community in building modern, &lt;a href=&quot;https://cableready.stimulusreflex.com/advocating-for-reactive-rails&quot;&gt;reactive&lt;/a&gt; Rails applications without needing the complexity of an API + SPA.&lt;/p&gt;

&lt;p&gt;Although Hotwire’s &lt;a href=&quot;https://turbo.hotwired.dev/&quot;&gt;Turbo&lt;/a&gt; library garnered a lot of attention, the Rails community has been working for years to improve the toolset we have to build modern, full-stack Rails applications. Turbo isn’t the first attempt at giving Rails developers the tools they need.&lt;/p&gt;

&lt;p&gt;One of the most important of these projects is &lt;a href=&quot;https://cableready.stimulusreflex.com/&quot;&gt;CableReady&lt;/a&gt;, which powers &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;StimulusReflex&lt;/a&gt; and &lt;a href=&quot;https://optimism.leastbad.com/&quot;&gt;Optimism&lt;/a&gt;, along with standing on its own as a tool to:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Create great real-time user experiences by triggering client-side DOM changes, events and notifications over &lt;a href=&quot;https://guides.rubyonrails.org/action_cable_overview.html&quot;&gt;ActionCable&lt;/a&gt; web sockets&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;font-size: 12px; text-align: right; margin-right: 10px;&quot;&gt;Source: &lt;a href=&quot;https://cableready.stimulusreflex.com/&quot; target=&quot;_blank&quot;&gt;CableReady Welcome&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today we’re going to explore CableReady by using Rails, CableReady, and &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; to build a scoreboard that updates for viewers in real-time, with just a few lines of Ruby and JavaScript.&lt;/p&gt;

&lt;p&gt;When we’re finished, our scoreboard will look like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/08-18-scoreboard-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with a web page and a terminal window open. On the page are the scores for two teams, Miami and Dallas. The user types a command in the terminal to update the home team&apos;s score to 95 and, after the command runs, the score on the web page for Miami updates to 95.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This article assumes that you’re comfortable working with Rails but you won’t need any prior knowledge of CableReady or ActionCable to follow along. If you’ve never used Rails before, this article isn’t the best place to start.&lt;/p&gt;

&lt;p&gt;Let’s dive in!
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application Setup&lt;/h2&gt;
&lt;p&gt;First, let’s create our Rails application, pull in CableReady and Stimulus, and scaffold up a Game model that we’ll use to power our scoreboard.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new scoreboard_ready &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;scoreboard_ready
bundle add cable_ready
yarn add cable_ready
rails webpacker:install:stimulus
rails g scaffold Game home_team:string away_team:string home_team_score:integer away_team_score:integer
rails db:migrate
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Although Redis is not technically required in development for CableReady, we’ll use it and &lt;a href=&quot;https://github.com/redis/hiredis-rb&quot;&gt;hiredis&lt;/a&gt; to match the installation guidance from CableReady.&lt;/p&gt;

&lt;p&gt;Update your Gemfile with these gems:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;redis&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;~&amp;gt; 4.0&apos;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Uncomment this line, it should already be in your Gemfile&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;hiredis&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; from your terminal.&lt;/p&gt;

&lt;p&gt;Don’t have Redis installed in your development environment? Installing Redis on your machine is outside the scope of this article, but you can find instructions for &lt;a href=&quot;https://redis.io/topics/quickstart&quot;&gt;Linux&lt;/a&gt; and &lt;a href=&quot;https://phoenixnap.com/kb/install-redis-on-mac&quot;&gt;OSX&lt;/a&gt; online.&lt;/p&gt;

&lt;p&gt;Finally, we need to update our ActionCable configuration in config/cable.yml to use Redis in development:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-yml&quot; data-lang=&quot;yml&quot;&gt;&lt;span class=&quot;na&quot;&gt;development&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;adapter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;redis&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV.fetch(&quot;REDIS_URL&quot;) { &quot;redis://localhost:6379/1&quot; } %&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;channel_prefix&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;scoreboard_ready_development&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;adapter&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;production&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;adapter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;redis&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&amp;lt;%= ENV.fetch(&quot;REDIS_URL&quot;) { &quot;redis://localhost:6379/1&quot; } %&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;channel_prefix&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;scoreboard_ready_production&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With the application setup complete, we’ll build the basic layout for our scoreboard next.&lt;/p&gt;

&lt;h2 id=&quot;setup-the-scoreboard-view&quot;&gt;Setup the scoreboard view&lt;/h2&gt;
&lt;p&gt;First, update the games show view:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-align: 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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;margin-top: 2rem;&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;s1&quot;&gt;&apos;Back to all games&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;games_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;/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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;game_detail&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;game: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@game&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re inlining a couple of styles to make the scoreboard a little more legible and rendering a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_detail&lt;/code&gt; partial that doesn’t exist yet. Add that next, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/games/_game_detail.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill it in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;h1&amp;gt;&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;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; vs. &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team&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;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;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;color: gray; font-size: 1.2rem; margin-bottom: 0.8rem;&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team&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;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team_score&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;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;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;color: gray; font-size: 1.2rem;&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team&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;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team_score&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Some more inline styles (we’ll remove these later!) with some standard erb to render each team’s name and score.&lt;/p&gt;

&lt;p&gt;At this point, we can go to &lt;a href=&quot;http://localhost:3000/games&quot;&gt;localhost:3000/games&lt;/a&gt; and create a game, and then go to the game show page to view it.&lt;/p&gt;

&lt;p&gt;We don’t have real-time updates in place yet, we’ll start building that with CableReady next.&lt;/p&gt;

&lt;h2 id=&quot;create-channel-and-controller&quot;&gt;Create channel and controller&lt;/h2&gt;
&lt;p&gt;Our first step to delivering real-time updates is to add a channel to broadcast updates on. When a user visits a game’s show page, they’ll be subscribed via a WebSocket connection to an ActionCable channel.&lt;/p&gt;

&lt;p&gt;Without a channel subscription, the CableReady broadcasts we’ll be sending soon won’t be received.&lt;/p&gt;

&lt;p&gt;To create a channel we can use the built-in generator:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails g channel Game&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This will create a few files for us. For the purposes of this article, we’re interested in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_channel.rb&lt;/code&gt; file created in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/channels&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Open that file and update the subscribed method:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;subscribed&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;stream_or_reject_for&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Game&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;id: &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;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscribed&lt;/code&gt; method is called each time a new &lt;a href=&quot;https://guides.rubyonrails.org/action_cable_overview.html#consumers&quot;&gt;Consumer&lt;/a&gt; connects to the channel.&lt;/p&gt;

&lt;p&gt;In this method, we’re using the &lt;a href=&quot;https://api.rubyonrails.org/v6.1.0/classes/ActionCable/Channel/Streams.html#method-i-stream_or_reject_for&quot;&gt;ActionCable method&lt;/a&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stream_or_reject_for&lt;/code&gt; to create a &lt;a href=&quot;https://guides.rubyonrails.org/action_cable_overview.html#streams&quot;&gt;Stream&lt;/a&gt; that will send subscribed users broadcasts for a specific instance of a game, based on an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;When no game is found, the subscription request will be rejected.&lt;/p&gt;

&lt;p&gt;With the channel built, next we need to allow consumers to subscribe to the channel so they can receive broadcasted updates.&lt;/p&gt;

&lt;p&gt;The channel generator we ran automatically creates a file at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javascripts/channels/game_channel.js&lt;/code&gt; that we could use to handle the subscription on the frontend; however, CableReady really shines when &lt;a href=&quot;https://cableready.stimulusreflex.com/leveraging-stimulus&quot;&gt;combined&lt;/a&gt; with Stimulus.&lt;/p&gt;

&lt;p&gt;To do that, we’ll create a new Stimulus controller, from the terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/javascript/controllers/game_controller.js&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill it in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;stimulus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;CableReady&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;cable_ready&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;nc&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Number&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nf&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;channel&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;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;consumer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subscriptions&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;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;channel&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;GameChannel&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;na&quot;&gt;id&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;idValue&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;received &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;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;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;cableReady&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;CableReady&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&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;nx&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;operations&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;span class=&quot;nf&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;channel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;unsubscribe&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This Stimulus controller is very close to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_channel.js&lt;/code&gt; created by the channel generator, with a little Stimulus and CableReady power added.&lt;/p&gt;

&lt;p&gt;Each time the Stimulus controller connects to the DOM, we create a new consumer subscription to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GameChannel&lt;/code&gt;, passing an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; parameter. When the Stimulus controller disconnects from the DOM, the subscription is removed.&lt;/p&gt;

&lt;p&gt;When a broadcast is received by the consumer, we use CableReady to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;perform&lt;/code&gt; the requested &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/operations&quot;&gt;operations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before the Stimulus controller will work, we need to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/index.js&lt;/code&gt; to import &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;consumer.js&lt;/code&gt; (part of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ActionCable&lt;/code&gt; package) and attach consumer to the Stimulus Application object.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;controllers/index.js&lt;/code&gt; with these two lines of code to accomplish that:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;consumer&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;../channels/consumer&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;consumer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;consumer&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Read more about why this is the right way to combine ActionCable and Stimulus &lt;a href=&quot;https://cableready.stimulusreflex.com/leveraging-stimulus#1-this-application-consumer&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With our Stimulus controller built, we can update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_detail&lt;/code&gt; partial to connect the controller to the DOM.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;&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;n&quot;&gt;game&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-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;game&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;data-game-id-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;game&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;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;h1&amp;gt;&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;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; vs. &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team&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;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;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;color: gray; font-size: 1.2rem; margin-bottom: 0.8rem;&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team&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;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team_score&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;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;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;color: gray; font-size: 1.2rem;&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team&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;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team_score&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we accomplished a lot with one change to the parent div:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;We attached the Stimulus controller to the parent div&lt;/li&gt;
  &lt;li&gt;Set the id &lt;a href=&quot;https://stimulus.hotwired.dev/reference/values&quot;&gt;value&lt;/a&gt; that the Stimulus controller uses to send the id param in the channel subscription request&lt;/li&gt;
  &lt;li&gt;Set the id of the div to the dom_id of the rendered game instance. We’ll use this id in the CableReady broadcast we’ll generate in our model, up next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With all of this in place, visit a game show page and check the Rails server logs. If everything is setup correctly, you should see log entries that look like this after the show page renders:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;GameChannel is transmitting the subscription confirmation
GameChannel is streaming from game:Z2lkOi8vc2NvcmVib2FyZC1yZWFkeS9HYW1lLzE&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;broadcast-game-updates-from-the-model&quot;&gt;Broadcast game updates from the model&lt;/h2&gt;
&lt;p&gt;With the channel built and consumers subscribing to updates, our last step to real-time scoreboard updates is sending a broadcast each time a game is updated.&lt;/p&gt;

&lt;p&gt;The simplest way to do this is to broadcast a CableReady operation in an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_update&lt;/code&gt; callback in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Game&lt;/code&gt; model.&lt;/p&gt;

&lt;p&gt;To make this possible, we first need to include the CableReady &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Broadcaster&lt;/code&gt; in our models and delegate calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;, as described in the (excellent) CableReady &lt;a href=&quot;https://cableready.stimulusreflex.com/cableready-everywhere#activerecord&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/application_record.rb&lt;/code&gt; as follows:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApplicationRecord&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;Base&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;abstract_class&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;CableReady&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Broadcaster&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;delegate&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:render&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;to: :ApplicationController&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/game.rb&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Game&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;after_update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cable_ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;GameChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;morph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;selector: &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;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;render&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;games/game_detail&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;game: &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;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;broadcast_to&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve added an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_update&lt;/code&gt; callback to trigger a CableReady &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast&lt;/code&gt;. The broadcast is sent on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GameChannel&lt;/code&gt;, queuing up a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;morph&lt;/code&gt; &lt;a href=&quot;https://cableready.stimulusreflex.com/reference/operations/dom-mutations#morph&quot;&gt;operation&lt;/a&gt; targeting the current game instance, and rendering the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_detail&lt;/code&gt; partial.&lt;/p&gt;

&lt;p&gt;With this callback in place, our scoreboard should now update in real-time.&lt;/p&gt;

&lt;p&gt;You can test this yourself by heading to a game show page and then opening your Rails console and running something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Game.find(some_id).update(home_team_score: 100)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You should see the score update in the browser window immediately after submitting the update command in the Rails console.&lt;/p&gt;

&lt;p&gt;While this works pretty well, our scoreboard really only needs to receive updates when the score changes, and it would be helpful to provide a little feedback to the user when the score changes.&lt;/p&gt;

&lt;p&gt;Let’s finish up this article by updating our implementation to broadcast only on score changes, and to animate newly updated scores.&lt;/p&gt;

&lt;h2 id=&quot;getting-fancier&quot;&gt;Getting fancier&lt;/h2&gt;
&lt;p&gt;To start, we’ve got some clunky inline styling that makes our erb code pretty hard to follow. Let’s move those styles out of the HTML and into a stylesheet. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;app/javascript/stylesheets
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/javascript/stylesheets/application.scss&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And add the below to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application.scss&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-scss&quot; data-lang=&quot;scss&quot;&gt;&lt;span class=&quot;nc&quot;&gt;.score-container&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;flex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;color&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;gray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;font-size&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;mi&quot;&gt;.2rem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;margin-bottom&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;mi&quot;&gt;.8rem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;nl&quot;&gt;justify-content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;center&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;nc&quot;&gt;.swing-in-top-fwd&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;-webkit-animation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;swing-in-top-fwd&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.5s&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cubic-bezier&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;mi&quot;&gt;.175&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.885&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.320&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.275&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;both&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;nl&quot;&gt;animation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;swing-in-top-fwd&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.5s&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cubic-bezier&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;mi&quot;&gt;.175&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.885&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.320&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.275&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;both&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;@-webkit-keyframes&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;swing-in-top-fwd&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-100deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-100deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&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;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&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;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@keyframes&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;swing-in-top-fwd&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-100deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-100deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&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;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rotateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;0deg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;-webkit-transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
            &lt;span class=&quot;nl&quot;&gt;transform-origin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;top&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;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To animate the scores, we’re just using a simple CSS swing animation, copy/pasted directly from the always handy &lt;a href=&quot;https://animista.net/play/entrances/swing-in&quot;&gt;Animista&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Finally import that new stylesheet into the webpack bundle:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// app/javascripts/application.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stylesheets/application&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We want to be able to update scores individually. To enable that, we’ll move the score portion of the scoreboard into a dedicated partial that we can then render in a broadcast.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/games/_score.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;&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;n&quot;&gt;team&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_score&quot;&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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;swing-in-top-fwd&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;score&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_detail&lt;/code&gt; partial to remove the inline styles and to use our new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;score&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;game&quot;&lt;/span&gt; 
  &lt;span class=&quot;na&quot;&gt;data-game-id-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;game&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;s&quot;&gt;&quot;&lt;/span&gt; 
  &lt;span class=&quot;na&quot;&gt;id=&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;n&quot;&gt;game&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;h1&amp;gt;&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;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; vs. &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;score-container&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
      &lt;span class=&quot;ss&quot;&gt;score: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;home_team_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
      &lt;span class=&quot;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;home&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;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;score-container&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
      &lt;span class=&quot;ss&quot;&gt;score: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;game&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;away_team_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
      &lt;span class=&quot;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;away&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;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we’ll update the callback in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Game&lt;/code&gt; model:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Game&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;after_update_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_changes&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;broadcast_changes&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;update_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;home&apos;&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;saved_change_to_home_team_score?&lt;/span&gt; 
    &lt;span class=&quot;n&quot;&gt;update_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;away&apos;&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;saved_change_to_away_team_score?&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;update_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;team&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cable_ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;GameChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;outer_html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;selector: &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;team&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;html: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;render&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;s1&quot;&gt;&apos;games/score&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;score: &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;send&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;team&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_team_score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;team: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;team&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;broadcast_to&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve updated our callback to check for changes to the two attributes we care about (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;home_team_score&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;away_team_score&lt;/code&gt;). When either attribute is changed, a broadcast is triggered from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_score&lt;/code&gt; to replace the target div’s contents with the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;score&lt;/code&gt; partial.&lt;/p&gt;

&lt;p&gt;We use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;outer_html&lt;/code&gt; CableReady operation in this case to completely replace the DOM content and ensure that our animation triggers when the new score content enters the DOM.&lt;/p&gt;

&lt;p&gt;And with that in place, we can now see our isolated, animated real-time updates:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/08-18-scoreboard-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with a web page and a terminal window open. On the page are the scores for two teams, Miami and Dallas. The user types a command in the terminal to update the home team&apos;s score to 95 and, after the command runs, the score on the web page for Miami updates to 95.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we explored how to add CableReady and Stimulus onto the core Rails ActionCable package to enable real-time DOM manipulations without writing tons of JavaScript, worrying about client-side state management, or doing much outside of writing pretty standard Rails code. The complete source code for this demo application is on &lt;a href=&quot;https://github.com/DavidColby/scoreboard_ready&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;CableReady (and StimulusReflex, which we’ll explore in a future article) are mature, powerful tools that allow Rails developers to create modern, reactive applications while keeping development time and code complexity low. CableReady also plays well with most of Turbo, and can fit seamlessly into a Hotwire-powered application.&lt;/p&gt;

&lt;p&gt;Start your journey deeper into CableReady and reactive Rails applications with these resources:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Read the &lt;a href=&quot;https://cableready.stimulusreflex.com/&quot;&gt;CableReady documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Read the &lt;a href=&quot;https://docs.stimulusreflex.com/&quot;&gt;StimulusReflex documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Read the &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Talk to the helpful folks on the &lt;a href=&quot;https://discord.gg/stimulus-reflex&quot;&gt;StimulusReflex discord&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Conditional Rendering With Turbo Stream Broadcasts</title>
   <link href="https://colby.so/posts/conditional-rendering-with-turbo-stream-broadcasts"/>
   <updated>2021-08-14T00:00:00+00:00</updated>
   <id>https://colby.so/posts/conditional-rendering-with-turbo-stream-broadcasts</id>
   <content type="html">&lt;p&gt;A very common pattern in Rails development is for a view to contain checks for things like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user.can?(:take_some_action)&lt;/code&gt;. These types of checks are common, especially in B2B applications that implement role-based permissions powered by a solution like &lt;a href=&quot;https://github.com/varvet/pundit&quot;&gt;Pundit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, naturally, when a Rails developer begins working with Turbo Stream broadcasts, they wonder how to access the  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt; or other session-level variables. The short answer? &lt;a href=&quot;https://discuss.hotwired.dev/t/authentication-and-devise-with-broadcasts/1752/4&quot;&gt;You can’t&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Partials rendered via a stream broadcast have to be free of global variables or they’ll throw an error because they are not rendered within the context of a specific request. Without that request context variables like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt; will always be undefined.&lt;/p&gt;

&lt;p&gt;While we can’t access global variables from a broadcast, we can, with creative use of Turbo Frames, still deliver real-time broadcasts that retain access to key variables like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;current_user&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To demonstrate the concept today we’re going to build a simple application that displays a list of Spies.&lt;/p&gt;

&lt;p&gt;Each spy will have a mission; however, not everyone will be able to see the spy’s mission. Only visitors with secret clearance can see the mission field. Everyone else will see a “Classified” string in place of the mission.&lt;/p&gt;

&lt;p&gt;When a new spy is created, we’ll broadcast an update to any viewers of the spy list. Viewers with clearance will see the mission for the newly created spy, viewers without won’t.&lt;/p&gt;

&lt;p&gt;The end result will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/08-15-hotwire-spies-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with two windows open to the same webpage. The webpage displays a list of names, with a header that reads Spies. In one window the user sees a message that they have secret clearance, in the other they don&apos;t. The user creates a new spy by entering a name and the new spy ges added to the list of spies already on the page. In the window with secret clearance, the user sees newly created spy&apos;s mission, in the other, they don&apos;t.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This article will be most useful for folks who are already comfortable with Ruby on Rails and who have a little experience with Turbo already. If you’re comfortable with Rails but you’ve never used Turbo before, an &lt;a href=&quot;https://www.colby.so/posts/remotely-loading-tab-content-with-rails-and-hotwire&quot;&gt;article like this&lt;/a&gt; might be a better introduction.&lt;/p&gt;

&lt;p&gt;As usual, you’ll find the full code for this article on &lt;a href=&quot;https://github.com/DavidColby/hotwire-spies&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ready? Let’s get started.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;application-setup&quot;&gt;Application Setup&lt;/h2&gt;

&lt;p&gt;Let’s get our application setup first, from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new hotwire-spies &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;hotwire-spies
bundle add hotwire-rails
rails hotwire:install
rails g scaffold Spy name:string mission:string
rails db:migrate 
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;setup-frames&quot;&gt;Setup Frames&lt;/h2&gt;
&lt;p&gt;We’re going to make some updates to the Spy index view that our scaffold generated to incorporate a few Turbo Frames and prepare to use broadcasts to append new records.&lt;/p&gt;

&lt;p&gt;Open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies/index.html.erb&lt;/code&gt; and update it:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;notice&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;notice&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;h1&amp;gt;&lt;/span&gt;Spies&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;n&quot;&gt;turbo_stream_from&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;spies&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;spies&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;@spies&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;spy&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;render&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;spy: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spy&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;br&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;s2&quot;&gt;&quot;spy_link&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re subscribing to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies&lt;/code&gt; turbo stream channel, setting an id on the container div for our rendered spies, and then looping through and rendering the spy partial for each spy.&lt;/p&gt;

&lt;p&gt;The default scaffold generator doesn’t create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy&lt;/code&gt; partial for us, so we need to create that:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/spies/_spy.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill it in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;n&quot;&gt;spy&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&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spy&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; | Current mission: &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mission&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We’re also rendering a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_link&lt;/code&gt; partial, which doesn’t exist yet. Go ahead and create it now:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/spies/_spy_link.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that it in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;new_spy&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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Spy&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_spy_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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Wrapping the new spy link in a dedicated frame means that when the link is clicked, we can replace the link on the page with the content returned from the call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/spies/new&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To make that work, we’ll need to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new.html.erb&lt;/code&gt; next to render its content in a matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; tag:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;new_spy&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;spy: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@spy&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In your browser, head to &lt;a href=&quot;http://localhost:3000/spies&quot;&gt;http://localhost:3000/spies&lt;/a&gt; and you should be able to click on the New Spy link and see the spy form render. Fill in a name and give your spy a mission, submit the form, and see that the form disappears but the spy doesn’t render on the page and the new spy link doesn’t come back.&lt;/p&gt;

&lt;p&gt;Our spy creation form POST succeeds, but we aren’t sending back a useful response from the controller or broadcast any updates on the Spies channel.&lt;/p&gt;

&lt;p&gt;Let’s finish up spy creation by tackling those issues next.&lt;/p&gt;

&lt;h2 id=&quot;handling-spy-creation&quot;&gt;Handling spy creation&lt;/h2&gt;
&lt;p&gt;When our new spy form is submitted, a turbo stream request is sent to the server, and right now our server isn’t responding back with anything useful. Let’s fix that first by heading to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpiesController&lt;/code&gt; and updating the create method:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@spy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Spy&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;spy_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@spy&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;c1&quot;&gt;# Our addition&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;turbo_stream&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;s2&quot;&gt;&quot;new_spy&quot;&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;spy_link&quot;&lt;/span&gt;&lt;span class=&quot;p&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@spy&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;Spy was successfully created.&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;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;p&quot;&gt;{&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;p&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;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;@spy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now our create method will respond to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; requests by replacing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new_spy&lt;/code&gt; frame with the content in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_link&lt;/code&gt; partial.&lt;/p&gt;

&lt;p&gt;Try creating a new spy now and, after a successful POST, the form should be removed from the page and the New Spy link should appear in its place.&lt;/p&gt;

&lt;p&gt;Finally, we need to append the newly created spy to the list of spies without the user needing to refresh the page.&lt;/p&gt;

&lt;p&gt;We’ll do that with a broadcast from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Spy&lt;/code&gt; model:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_append_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;spies&apos;&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;s1&quot;&gt;&apos;spies&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;spy: &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;p&quot;&gt;}&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This callback runs after a new spy is created and sends a broadcast on the Spies channel containing a Turbo Stream element that looks like this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream action=&quot;append&quot; target=&quot;spies&quot;&amp;gt;&lt;/code&gt;. Nested within the stream element is the with the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy&lt;/code&gt; partial&lt;/p&gt;

&lt;p&gt;Now when we create a new spy, the new record should instantly be added to the list of spies, no refresh required.&lt;/p&gt;

&lt;p&gt;With all of this in place, we can move to the really fun part of this article, conditionally rendering content for different users from a Turbo Stream broadcast.&lt;/p&gt;

&lt;p&gt;Let’s see how it works.&lt;/p&gt;

&lt;h2 id=&quot;conditional-rendering-from-a-broadcast&quot;&gt;Conditional rendering from a broadcast&lt;/h2&gt;
&lt;p&gt;First, let’s remember that our original goal was that each spy’s mission would only be displayed to users with secret clearance. Everyone else should just see “Classified” displayed in place of the mission text.&lt;/p&gt;

&lt;p&gt;This is simple to do on the initial page load, but since we’re broadcasting new agent creation via a WebSocket connection, things are a little more complicated. Without access to the user’s session, we’ll have to get creative.&lt;/p&gt;

&lt;p&gt;To meet our secret clearance requirement, we’re going to use a technique that I first saw described on the &lt;a href=&quot;https://discuss.hotwired.dev/t/proposal-frame-reloads-through-streams/2209&quot;&gt;Hotwire discussion forums&lt;/a&gt; that allows us to bypass the need to access session-level variables in partials rendered by a broadcast.&lt;/p&gt;

&lt;p&gt;We’ll implement this requirement by adding a check for a session variable into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy&lt;/code&gt; partial that our broadcast renders. When the session variable exists, the user will see mission text, otherwise the user will not.&lt;/p&gt;

&lt;p&gt;This session variable is a rough mock of what, in a real application, might be accessed through something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Current.user&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To start, we’ll add a helper method to our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt; to make it easy to access this session variable:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;helper_method&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:secret_clearance&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;secret_clearance&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:clearance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;presence&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then we’ll reference that helper method in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies/_spy.html.erb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;n&quot;&gt;spy&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&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spy&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; | Current mission: &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;secret_clearance&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;mission&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Classified&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We’ll need a way to set the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clearance&lt;/code&gt; for different users. To do that, we’ll add a new endpoint to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpiesController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, update our routes file:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:spies&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;collection&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;clearance&apos;&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then in the controller, add the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clearance&lt;/code&gt; method:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;clearance&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;secret_clearance&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:clearance&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;session&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:clearance&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;kp&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spies_path&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then, we’ll add a way to the set the clearance session variable from the index page in our extremely secure spy application.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;Your current clearance is &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;secret_clearance&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Secret&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;None&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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Change clearance&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clearance_spies_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :post&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Before we move on, create a new spy from the Spies index page and see that secret clearance is always false when broadcasting through a channel, even when the current session has secret clearance set to true.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/08-15-hotwire-spies-unfinished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with on a webpage. The webpage has a header that reads Spies, and lists the names of a few spies. At the top of the page, text informs the user that they have secret clearance. The user types in the name and mission of a new spy and the new spy is added to the page, but the spy&apos;s mission is listed as classified.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is happening because the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;session&lt;/code&gt; when rendering from a broadcast is an empty hash. The session isn’t read from the user’s current session, and so our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;secret_clearance&lt;/code&gt; check will always fail.&lt;/p&gt;

&lt;p&gt;To fix this issue and ensure users always see what they’re supposed to see, we’ll start by creating a new partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/spies/_spy_frame.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill that new partial in with the following content:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;n&quot;&gt;spy&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;spy_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;frame: &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;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re taking advantage of the fact that Turbo Frames &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#lazy-loaded-frame&quot;&gt;can be given&lt;/a&gt; an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute which causes the frame load its content from a URL when it is inserted onto the page.&lt;/p&gt;

&lt;p&gt;In this case, the frame will retrieve the content from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies#show&lt;/code&gt;. We’ll update that method in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpiesController&lt;/code&gt; next:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;turbo-frame&quot;&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;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;spy&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;spy: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@spy&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;render&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;show&apos;&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our show method checks the request headers to see if the request includes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-frame&lt;/code&gt; header. When the frame header is present, the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy&lt;/code&gt; partial is rendered.&lt;/p&gt;

&lt;p&gt;Finally, update our model broadcast to render the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_frame&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_append_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;spies&apos;&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;s1&quot;&gt;&apos;spies&apos;&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;s1&quot;&gt;&apos;spies/spy_frame&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;agent: &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;p&quot;&gt;}&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With these changes in place, everything should work. To test it out, first make sure you’ve got two sessions active (open one in an incognito window). In one browser window, give the user clearance, in the other don’t.&lt;/p&gt;

&lt;p&gt;Create a new spy in either session, and see that the broadcast triggers an update in both windows. In the window with clearance, the mission should display, in the window without clearance, Classified displays instead, like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/08-15-hotwire-spies-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user with two windows open to the same webpage. The webpage displays a list of names, with a header that reads Spies. In one window the user sees a message that they have secret clearance, in the other they don&apos;t. The user creates a new spy by entering a name and the new spy ges added to the list of spies already on the page. In the window with secret clearance, the user sees newly created spy&apos;s mission, in the other, they don&apos;t.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;So what’s going on here?&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;The broadcast sends the empty, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; powered frame from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy_frame&lt;/code&gt; partial to all subscribed users and that frame is rendered safely since it does not request any session variables&lt;/li&gt;
  &lt;li&gt;When the frame is inserted onto each user’s page, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src&lt;/code&gt; attribute on the frame immediately triggers an HTTP request to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spies/:id&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Since the request is sent from the browser to an endpoint on the server, we have access to all the session data we expect in the rendered response&lt;/li&gt;
  &lt;li&gt;The server responds to the request with the content of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spy&lt;/code&gt; partial which includes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; with an id that matches the frame id of the empty frame in step 1.&lt;/li&gt;
  &lt;li&gt;Since the frame ids match, Turbo replaces the content of the frame with the response from the server, and the new spy appears in the list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this trick, we can now make use of Turbo Stream broadcasts in situations where the rendered content changes based on non-local variables. Magical.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Today we looked at a potential solution to a very common blocker for folks building more complex applications with Hotwire.&lt;/p&gt;

&lt;p&gt;Real-world use cases for this approach include conditionally rendering edit/delete controls based on user permissions, or not displaying certain information to users without appropriate permissions inside of a multi-user account.&lt;/p&gt;

&lt;p&gt;Other recommended approaches to these types of problems typically rely on using inline styles to hide/show content but that is not an acceptable solution for all use cases, particularly in more sensitive applications where the data being included in the markup but hidden on the page isn’t acceptable.&lt;/p&gt;

&lt;p&gt;Although this method works well for some use cases, it certainly isn’t without downsides or limitations, which include:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Each broadcast now requires an additional round trip to the server. We’re gaining a lot of flexibility in return for additional load on our server but we are adding load and potentially slowing down our application. Be sure you need the extra flexibility gained by this approach.&lt;/li&gt;
  &lt;li&gt;The complexity of your code is increased. Following what’s happening in a broadcast takes a little more effort, even for the simplest possible use case outlined in this article.&lt;/li&gt;
  &lt;li&gt;If you’re using this method to update content that already exists on the page, you might see flickering issues. There are ways to address this, including one described in the discussion on this approach on the &lt;a href=&quot;https://discuss.hotwired.dev/t/proposal-frame-reloads-through-streams/2209/7&quot;&gt;Hotwire forum&lt;/a&gt;. Depending on your UX needs, you could also include an “updating content” loading state in the frame broadcast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When considering taking this approach in a production application, think about the tradeoff you’re making. We learned about a hammer today, but that doesn’t mean every problem related to session variables and broadcasts is a nail.&lt;/p&gt;

&lt;p&gt;Sometimes, you might be better off with another approach like:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Broadcasting a generic “refresh to see changes” message&lt;/li&gt;
  &lt;li&gt;Using inline styles to conditionally show/hide information&lt;/li&gt;
  &lt;li&gt;Adjusting the content rendered in a broadcast to eliminate the need to access session variables&lt;/li&gt;
  &lt;li&gt;Scoping your streams appropriately to ensure that every recipient of a particular broadcast should see what is being broadcast&lt;/li&gt;
  &lt;li&gt;Not broadcasting updates at all. Sometimes, real-time visibility into changes isn’t helpful or necessary and folks will be comfortable seeing the updated information on the next page load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While not without tradeoffs and potential drawbacks, this technique demonstrates some of the power that can be unlocked with creative applications of Turbo Streams and Frames in Rails applications. With Turbo still in early days, we can expect to see even more powerful techniques developed in the coming years to continue to push Turbo and Rails applications powered by Turbo ahead.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Turbo Streams on Rails</title>
   <link href="https://colby.so/posts/turbo-streams-on-rails"/>
   <updated>2021-08-09T00:00:00+00:00</updated>
   <id>https://colby.so/posts/turbo-streams-on-rails</id>
   <content type="html">&lt;p&gt;&lt;em&gt;Updated March 17th, 2024 to account for the release of refresh and morph Stream actions in Turbo 8&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Turbo, part of the Hotwire tripartite, helps you build modern, performant web applications with dramatically less custom JavaScript.&lt;/p&gt;

&lt;p&gt;Turbo is composed of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Drive&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Frames&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Streams&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Strada&lt;/code&gt;. Each is a valuable piece of the puzzle but today we’re going to focus on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Turbo Streams&lt;/code&gt;.
Turbo Streams “&lt;a href=&quot;https://turbo.hotwired.dev/&quot;&gt;deliver page changes over WebSocket, Server-Sent Events, or in response to form submissions&lt;/a&gt;” and they might be the most exciting part of the Turbo package.&lt;/p&gt;

&lt;p&gt;Streams allow you to deliver fast and efficient user experiences with minimal effort and, for Rails developers, you’ll find that streams fit comfortably into your existing toolkit. With just a few lines of code, you can send tiny snippets of HTML over the wire and and use those snippets to update the DOM with no custom JavaScript.&lt;/p&gt;

&lt;p&gt;For fancier use cases, you can broadcast changes over a WebSocket connection to keep every interested user updated in real-time. No custom JavaScript to write, just HTML and a little bit of Ruby.&lt;/p&gt;

&lt;p&gt;Let’s dive in.&lt;/p&gt;
&lt;h2 id=&quot;whats-a-turbo-stream&quot;&gt;What’s a Turbo Stream?&lt;/h2&gt;

&lt;p&gt;At their core, streams allow you to send snippets of HTML to the browser to make small changes to the DOM in response to events that happen on the server.&lt;/p&gt;

&lt;p&gt;The basic structure of a Turbo Stream is an HTML snippet that looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;action_to_take&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;element_to_update&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;template&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;element_to_update&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;!-- Some more html --&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;/template&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; element always includes an action, from the following possibilities:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;append&lt;/li&gt;
  &lt;li&gt;prepend&lt;/li&gt;
  &lt;li&gt;before&lt;/li&gt;
  &lt;li&gt;after&lt;/li&gt;
  &lt;li&gt;replace&lt;/li&gt;
  &lt;li&gt;update&lt;/li&gt;
  &lt;li&gt;remove&lt;/li&gt;
  &lt;li&gt;morph&lt;/li&gt;
  &lt;li&gt;refresh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In addition to an action, a target (or targets) must be provided for all actions except for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refresh&lt;/code&gt;. Refreshing and morphing are special cases introduced in Turbo 8, and you can read more about morphing and Turbo Stream refresh broadcasts in &lt;a href=&quot;https://www.colby.so/posts/turbo-8-morphing-refreshes-on-rails&quot;&gt;this article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Inside of the stream element, the HTML to render gets wrapped in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag.
Turbo’s JavaScript processes these HTML snippets by reading the action and target from the stream tag and inserting (or removing) the wrapped HTML as appropriate.&lt;/p&gt;

&lt;p&gt;Turbo Streams can be sent in response to a direct request from a browser (like submitting a form) or broadcast to subscribers over a WebSocket connection.&lt;/p&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt;, we have the tools we need to take either path, and we’ll look at both methods in detail next.&lt;/p&gt;
&lt;h2 id=&quot;turbo-streams-in-controllers&quot;&gt;Turbo Streams in controllers&lt;/h2&gt;

&lt;p&gt;The simplest way to get started with Turbo Streams is to have a controller respond to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; requests and render a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; response in return.&lt;/p&gt;

&lt;p&gt;This approach looks something like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# players_controller.rb&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;@player&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;player_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@player&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;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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@player&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;Player was successfully created.&quot;&lt;/span&gt; &lt;span class=&quot;p&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;turbo_stream&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&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;p&quot;&gt;{&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;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;c1&quot;&gt;# app/views/players/create.turbo_stream.erb&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%= turbo_stream.replace &quot;players_form&quot; do %&amp;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;new_player&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;% end &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In this example, a user is attempting to save a new player record in the database.&lt;/p&gt;

&lt;p&gt;When the save is successful and the request format is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt;, we use Rails’ implicit rendering to respond with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt; which renders a Turbo Stream response with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; action targeting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players_form&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/ba86832f7f13001793ab917185788df9723666e8/app/helpers/turbo/streams_helper.rb&quot;&gt;turbo_stream&lt;/a&gt; method in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt; view generates the necessary &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; tag and wraps the content within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; block in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;template&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;The response back to the browser will look like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;replace&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players_form&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;template&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- Content of new_player.html.erb partial --&amp;gt;&lt;/span&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;players_form&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/players/new&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;New Player&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Turbo’s JavaScript then steps in, processes the response, and updates the DOM.
We do not need to make any adjustments to the failure path in our controller above. Even though the form submission is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; request, Rails falls back to responding with HTML when a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; format to respond with does not exist.&lt;/p&gt;

&lt;p&gt;Our example demonstrates handling a POST request with Turbo Streams, but we can use the same approach with a GET request, add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-turbo-stream&lt;/code&gt; to the &lt;a href=&quot;https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses&quot;&gt;link or GET form&lt;/a&gt; you want to send as a Turbo Stream request and Turbo will handle the rest.&lt;/p&gt;

&lt;h2 id=&quot;rendering-inline&quot;&gt;Rendering inline&lt;/h2&gt;

&lt;p&gt;For simple responses we do not always need dedicated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.erb&lt;/code&gt; files. Instead we can render the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; response inline &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/streams/turbo_streams_tag_builder.rb&quot;&gt;in the controller&lt;/a&gt;, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@player&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;player_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@player&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;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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;redirect_to&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@player&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;Player was successfully created.&quot;&lt;/span&gt; &lt;span class=&quot;p&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;turbo_stream&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;s1&quot;&gt;&apos;players_form&apos;&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;s1&quot;&gt;&apos;new_player&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;k&quot;&gt;else&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;p&quot;&gt;{&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;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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There is no functional difference between inline rendering and rendering from a template, for simple actions you can use whichever feels better.&lt;/p&gt;

&lt;h2 id=&quot;updating-multiple-elements-at-once&quot;&gt;Updating multiple elements at once&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create.turbo_stream.erb&lt;/code&gt; file we saw earlier renders a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; tag, but we aren’t limited to updating a single stream per response. We can update multiple elements at once by simply adding them to our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream.erb&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;players_form&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;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;new_player&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;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;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;s2&quot;&gt;&quot;players&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;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;player: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@player&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now after a successful form POST, we will send back a response with two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; tags, each of which will be processed by Turbo’s JavaScript on the front end.&lt;/p&gt;

&lt;p&gt;If you really dislike creating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; templates, you can also render multiple Streams inline in the controller like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;turbo_stream&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Pass an array of turbo_streams to the render call&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;p&quot;&gt;[&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;s1&quot;&gt;&apos;players_form&apos;&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;s1&quot;&gt;&apos;new_player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&apos;&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;s1&quot;&gt;&apos;player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;player: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@player&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Should you do this? &lt;a href=&quot;https://github.com/hotwired/turbo-rails/issues/77#issuecomment-757349251&quot;&gt;Probably not&lt;/a&gt;, but it works.&lt;/p&gt;

&lt;h2 id=&quot;broadcasting-changes&quot;&gt;Broadcasting changes&lt;/h2&gt;

&lt;p&gt;So far, we have seen rendering streams from controller actions. This method works great for dealing with form submissions made by individual users; however, there is another method of broadcasting updates to a wider audience that you can call on when needed.&lt;/p&gt;

&lt;p&gt;As we saw in the Hotwire introduction video, stream broadcasts sent from the controller only update the page that made the original request. If other people are viewing the same page, they won’t see the update until they refresh the page.&lt;/p&gt;

&lt;p&gt;If your use case requires every interested user to see changes as they happen, you can use &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/ba86832f7f13001793ab917185788df9723666e8/app/channels/turbo/streams/broadcasts.rb&quot;&gt;broadcasts&lt;/a&gt; from the turbo-rails gem to send streams to every subscriber at once, in real time.&lt;/p&gt;

&lt;p&gt;The basic syntax for these updates looks like this, in your model:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# app/models/player.rb&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_append_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re using a callback, fired each time a new player is created, to broadcast the newly created player to the players channel.&lt;/p&gt;

&lt;h2 id=&quot;broadcast-options&quot;&gt;Broadcast options&lt;/h2&gt;

&lt;p&gt;By default, the Stream target will be set to the &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/ba86832f7f13001793ab917185788df9723666e8/app/models/concerns/turbo/broadcastable.rb#L68&quot;&gt;plural name of the model&lt;/a&gt;; however you can override the target as needed by passing in a target to the method, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_append_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&apos;&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;s1&quot;&gt;&apos;players_list&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Broadcasting from the model will attempt to use &lt;a href=&quot;https://github.com/rails/rails/blob/5411787a155e2d1c633a74314edc164d5b300c34/activemodel/lib/active_model/conversion.rb#L103&quot;&gt;to_partial_path&lt;/a&gt; to guess the name of the partial that should be rendered.&lt;/p&gt;

&lt;p&gt;In our examples so far, if we have a partial in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/players&lt;/code&gt; named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_player.html.erb&lt;/code&gt; that partial will be used. As with targeting, you can override the partial like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_append_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&apos;&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;s1&quot;&gt;&apos;a_special_player&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When streaming in this manner, you must create a WebSocket connection to the channel you’re broadcasting updates on.&lt;/p&gt;

&lt;p&gt;To do that, you can add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;%= turbo_stream_from &quot;some_channel&quot; %&amp;gt;&lt;/code&gt; to the view. The name of the channel passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream_from&lt;/code&gt; must match the name of the channel passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_action_to&lt;/code&gt; , otherwise your updates will get lost in space.&lt;/p&gt;

&lt;h2 id=&quot;broadcast_action_to-vs-broadcast_action_later_to&quot;&gt;broadcast_action_to vs. broadcast_action_later_to&lt;/h2&gt;

&lt;p&gt;In the source code for &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb&quot;&gt;broadcastable&lt;/a&gt;, a &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/102a491754d46f7dd924201fcfaf879a0f04b11c/app/models/concerns/turbo/broadcastable.rb#L72&quot;&gt;comment near the top of the file&lt;/a&gt; advises us to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_action_later_to&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_action_to&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_later&lt;/code&gt; methods moves the stream broadcast into a &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/ba86832f7f13001793ab917185788df9723666e8/app/channels/turbo/streams/broadcasts.rb#L67&quot;&gt;background job&lt;/a&gt;, which means that broadcasts can happen asynchronously, moving the potentially expensive rendering work out of the web request.&lt;/p&gt;

&lt;p&gt;Since we want everything to be fast, not slow, we’ll use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_action_later_to&lt;/code&gt;, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_append_later_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;players&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The only exception to the later rule is delete actions. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_remove_to&lt;/code&gt; simply removes an element from the DOM without rendering a template and so does not need to be moved into a background job. To reinforce this exception, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_remove_later_to&lt;/code&gt; is not defined and calling it will throw an undefined method error.&lt;/p&gt;

&lt;h2 id=&quot;broadcasting-from-a-controller&quot;&gt;Broadcasting from a controller&lt;/h2&gt;

&lt;p&gt;So far we’ve seen that we can render a Turbo Stream response from a controller action and broadcast from a model.&lt;/p&gt;

&lt;p&gt;Another, less common, path is to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_append|remove|replace&lt;/code&gt; on an instance of a model from within a controller (or a service, or anywhere else you like), like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;vi&quot;&gt;@player&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;broadcast_append_later_to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This does the same thing as calling this method in a callback in the model, meaning the append will broadcast to all users subscribed to the players channel.&lt;/p&gt;

&lt;h2 id=&quot;more-magical-methods&quot;&gt;More magical methods&lt;/h2&gt;

&lt;p&gt;So far we’ve been adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_to&lt;/code&gt; at the end of our broadcast methods and explicitly passing in the channel name. If you like magic, you can omit the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_to&lt;/code&gt; from your broadcasts and just use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_append|remove|replace&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using this form requires a channel streaming from the model instance, like the below.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# player.rb&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;after_update_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_replace&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# _player.html.erb&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%= turbo_stream_from @player %&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;To type even less, you can add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcasts_to&lt;/code&gt; to your model, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# player.rb&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;broadcasts_to&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;_&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;players&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re using a non-standard stream name of players. If we’re using the model instance as the stream name, we can get down to a single magic word:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# player.rb&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;broadcasts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcasts&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcasts_to&lt;/code&gt; automatically add broadcasts on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;create&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt; commits to the model, as seen &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/102a491754d46f7dd924201fcfaf879a0f04b11c/app/models/concerns/turbo/broadcastable.rb#L183&quot;&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, these methods often do not add much value since they rely so much on magical naming convention.&lt;/p&gt;

&lt;p&gt;The magic fails when attempting to, for example, append a newly created record to an index list. In that scenario, our stream won’t be tied to an instance of the class, and so we’ll need to use the long form version of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_action_to&lt;/code&gt;, like we saw above.&lt;/p&gt;

&lt;p&gt;When in doubt, just use the longer, slightly less magical methods. I’m sharing these magic methods because they’re commonly used in guides and feature prominently in the announcement video so you’ll likely find them in the wild for years to come. If your use case allows you to use them, go for it, but the longer versions work just fine too.&lt;/p&gt;

&lt;h2 id=&quot;gotchas&quot;&gt;Gotchas&lt;/h2&gt;
&lt;h3 id=&quot;scope-your-channels&quot;&gt;Scope your channels!&lt;/h3&gt;
&lt;p&gt;In most of the examples I’ve shown so far, we are broadcasting to streams with hardcoded strings for names (“players”). This works fine; however, in most web applications resources aren’t globally available. Account 123 should only see updates on their account, not every account.
You can avoid streaming updates to the wrong user by ensuring you’re broadcasting to properly scoped channels and subscribing users to those scoped channels.&lt;/p&gt;

&lt;p&gt;In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; example code we’ve used so far, we can imagine that a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player&lt;/code&gt; belongs to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;team&lt;/code&gt;, and updates to those players should be broadcast on the team channel.&lt;/p&gt;

&lt;p&gt;To implement this in our code, it might look like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# teams/show.html.erb&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%= turbo_stream_from @team %&amp;gt;

# player.rb
belongs_to :team

after_create_commit { broadcast_append_later_to(team) }
after_destroy_commit { broadcast_remove_to(team) }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With this in place, the team show page subscribes to updates related to that specific team, keeping data from leaking between different teams. The same concept can be applied to scope channels by user or account, as needed.&lt;/p&gt;

&lt;p&gt;Note that this scoping is only necessary for broadcasts sent over WebSocket. If your controller renders a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; response, only the client that made the initial request will receive the update. There is no risk of data leaking in this manner when responding to a form submission with a turbo stream template.&lt;/p&gt;

&lt;h2 id=&quot;error-handling&quot;&gt;Error handling&lt;/h2&gt;
&lt;p&gt;When you render a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; from a controller action and the DOM doesn’t have a matching target element to update… nothing happens. There’s no error anywhere, the stack trace indicates that the partial was rendered, and the DOM doesn’t update.&lt;/p&gt;

&lt;p&gt;This is normal, expected behavior. There was not an error, there just wasn’t anything to change in the DOM - but it can be confusing when you’re getting started and the lack of errors can make troubleshooting more difficult.&lt;/p&gt;

&lt;p&gt;If you are not getting errors, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_stream&lt;/code&gt; response is showing in the server logs, and nothing is updating the page, the problem is (almost) always that the target passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; isn’t matching any elements in your page’s markup. Start by checking what you are rendering on the page and compare that to the target passed to the stream.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;Turbo is an incredibly powerful tool, and the tight integration between Rails and Turbo means that forward-looking Rails developers should be exploring ways to bring Turbo into their applications.&lt;/p&gt;

&lt;p&gt;Using streams with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails&lt;/code&gt; opens up a world of possible user experiences for Rails developers that previously meant more work, more effort, and more code to maintain.&lt;/p&gt;

&lt;p&gt;Hopefully what I’ve shared here helps you feel a little more comfortable thinking about streams, and how streams can fit into your existing code base. As always, thanks for reading!
More resources
Ready to dive deeper?&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Spend some time with &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;turbo-rails&lt;/a&gt; source code&lt;/li&gt;
  &lt;li&gt;Spend some time with the &lt;a href=&quot;https://github.com/hotwired/turbo&quot;&gt;turbo&lt;/a&gt; source code&lt;/li&gt;
  &lt;li&gt;Dive into the &lt;a href=&quot;https://turbo.hotwired.dev/&quot;&gt;docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Read some practical guides, like &lt;a href=&quot;https://www.colby.so/posts/instant-search-with-rails-6-and-hotwire&quot;&gt;this guide&lt;/a&gt; on building a live search interface, or &lt;a href=&quot;https://www.colby.so/posts/handling-modal-forms-with-rails-and-hotwire&quot;&gt;this one&lt;/a&gt; on handling modal form submissions&lt;/li&gt;
  &lt;li&gt;GoRails has &lt;a href=&quot;https://gorails.com/episodes?q=hotwire&quot;&gt;published&lt;/a&gt; a variety of Hotwire-specific guides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, if you want to learn about implementing Turbo Streams in a complex Rails application, I wrote &lt;a href=&quot;https://davidcolby.gumroad.com/l/hotwired-ats&quot;&gt;a book&lt;/a&gt; on building a modern Ruby on Rails application with Hotwire. The book is written in this same, step-by-step tutorial style that you find throughout this blog and aims to offer solutions for a variety of common, real-world web application problems without reaching for the weight of a full frontend framework.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Instant search with Rails and Hotwire</title>
   <link href="https://colby.so/posts/instant-search-with-rails-6-and-hotwire"/>
   <updated>2021-08-05T00:00:00+00:00</updated>
   <id>https://colby.so/posts/instant-search-with-rails-6-and-hotwire</id>
   <content type="html">&lt;aside style=&quot;padding: 1rem; border-radius: .375; background-color: #EFF6FF;&quot;&gt;
  &lt;p style=&quot;color: #1D4ED8; line-height: 1.25rem; font-size: 0.875rem;&quot;&gt;An updated version of this article for Rails 7 and Turbo 8 was published in March of 2024. Everything in this article still works, but the updated article presents a simpler path with the new tools added in Turbo 8. Read the &lt;a href=&quot;https://www.colby.so/posts/turbo-8-search-and-filter&quot;&gt;updated article here&lt;/a&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Last year I wrote &lt;a href=&quot;https://www.colby.so/posts/live-search-with-stimulus-and-rails-6&quot;&gt;an article&lt;/a&gt; on building an instant search form with Rails and Stimulus. Since then, &lt;a href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;Turbo&lt;/a&gt;, the other half of &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire&lt;/a&gt; for the web has been released and the Hotwire stack is now the default for new applications in Rails 7.&lt;/p&gt;

&lt;p&gt;Turbo opens the door for an even simpler, cleaner implementation of an instant search form.&lt;/p&gt;

&lt;p&gt;So, today we’re taking another look at how to build a search-as-you-type interface that allows us to query our database for matches and update the UI with those matches (almost) instantly.&lt;/p&gt;

&lt;p&gt;When we’re done, our code will look almost identical to what you’re used to writing to build Rails applications, and the only JavaScript we’ll need will be to trigger form submissions as the user types.&lt;/p&gt;

&lt;p&gt;The finished product will work like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-12-31-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing in a word to search a list of Dallas Mavericks basketball players. As they type, the list of players updates automatically.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I’m writing this assuming that you’re comfortable with Ruby on Rails - you won’t need any knowledge of the Hotwire stack and you’ll find the most value from this article if you’re new to Turbo. While this article uses Stimulus, its core focus is on Turbo, and specifically how to use Turbo Frames in a Rails application.&lt;/p&gt;

&lt;p&gt;You can find the complete code for this guide on &lt;a href=&quot;https://github.com/DavidColby/turbo-frame-searching-rails&quot;&gt;Github&lt;/a&gt;. Follow along with this guide from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; branch, or see the complete implementation in the &lt;a href=&quot;https://github.com/DavidColby/turbo-frame-searching-rails/tree/instant-search&quot;&gt;instant search&lt;/a&gt; branch.&lt;/p&gt;

&lt;p&gt;Let’s get started.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h3 id=&quot;application-setup&quot;&gt;Application Setup&lt;/h3&gt;
&lt;p&gt;First, we’ll need an application with both Turbo and Stimulus installed and ready to go. If you’ve already got an application setup, feel free to use that instead.&lt;/p&gt;

&lt;p&gt;To follow along line-by-line, start by running these commands from your terminal. Your machine will need to be setup for standard Rails development, with Ruby and Rails. For reference, I’m writing this guide using Rails 7 and Turbo 7.1.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot; data-lang=&quot;shell&quot;&gt;rails new hotwire-search &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;hotwire-search
rails g scaffold Player name:string
rails db:migrate
rails s
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If you’re using Rails 6 or working from an existing application, you’ll also need to install Turbo and Stimulus. The simplest way to do this is to use the &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;turbo-rails&lt;/a&gt; and &lt;a href=&quot;https://github.com/hotwired/stimulus-rails&quot;&gt;stimulus-rails&lt;/a&gt; gems.&lt;/p&gt;

&lt;p&gt;With the above commands run, we should have a new rails application up and running, and visiting &lt;a href=&quot;https://localhost:3000/players&quot;&gt;https://localhost:3000/players&lt;/a&gt; should open a page that lists the players in your database and includes a link to create new players.&lt;/p&gt;

&lt;p&gt;Go ahead and add a few new players from the UI, or open up your Rails console and add some players there.&lt;/p&gt;

&lt;p&gt;Give them different names since we’ll be filtering the list of players by name in this project.&lt;/p&gt;

&lt;h3 id=&quot;add-the-search-form-and-controller-action&quot;&gt;Add the search form and controller action&lt;/h3&gt;
&lt;p&gt;With setup complete, open up the code in your favorite editor.&lt;/p&gt;

&lt;p&gt;We’ll start by updating the players index view to add a form that we’ll use to filter the list of players.&lt;/p&gt;

&lt;p&gt;Update the players index view to add the form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;color: green&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;notice&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;h1&amp;gt;&lt;/span&gt;Players&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;n&quot;&gt;form_with&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;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;GET&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search by name:&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Search&apos;&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;kp&quot;&gt;nil&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;players&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;@players&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;player&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;render&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player&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;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Show this player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player&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;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;New player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; action in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; to process search form submissions:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;k&quot;&gt;if&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;:query&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;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name LIKE ?&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;At this point, our search form works — submit it and the page will reload with the list of players filtered to match your search query; however, each form submission has to be manually triggered and the entire page reloads instead of only rendering the new list of players. We won’t win any awards for the elegance of our code, but it works just fine and will serve as a simple base for us to build from.&lt;/p&gt;

&lt;p&gt;Let’s start with adding rendering our search results with Turbo Frames first, and then we’ll wrap up by adding a Stimulus controller to submit the form automatically as the user types.&lt;/p&gt;

&lt;h3 id=&quot;adding-turbo-frames&quot;&gt;Adding Turbo Frames&lt;/h3&gt;
&lt;p&gt;Our goal is to use a Turbo Frame to update only the content of the list of players each time the form is submitted. To do this, we’ll first need to wrap the content we want to update in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; HTML tag.&lt;/p&gt;

&lt;p&gt;We can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb#L26&quot;&gt;helper method&lt;/a&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-rails &lt;/code&gt; for this.&lt;/p&gt;

&lt;p&gt;We’ll first create &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial that renders the list of players wrapped in a turbo frame.&lt;/p&gt;

&lt;p&gt;From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/_players.html.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then fill that in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;players&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;n&quot;&gt;players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re rendering a collection of players, wrapped inside of a turbo frame tag.&lt;/p&gt;

&lt;p&gt;Next update the players index to render the partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;p&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;color: green&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;notice&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;h1&amp;gt;&lt;/span&gt;Players&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;n&quot;&gt;form_with&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;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;GET&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search by name:&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Search&apos;&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;kp&quot;&gt;nil&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;c&quot;&gt;&amp;lt;!-- Rendering the new partial here --&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;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;players: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@players&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;s2&quot;&gt;&quot;New player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;player&lt;/code&gt; partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;&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;n&quot;&gt;player&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;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;player&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;View Player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;player_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;player&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;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With the turbo frame in place, refresh the page and see that it looks identical to what we had before but in the markup you’ll see a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; element wrapping the list of players:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;players&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;!-- Snip player divs --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’re wrapping our players in a Turbo Frame, but we’re not making use of that Turbo Frame on the server.&lt;/p&gt;

&lt;h3 id=&quot;rendering-a-frame-from-the-server&quot;&gt;Rendering a Frame from the server&lt;/h3&gt;
&lt;p&gt;To do that, update the index action in the players controller to check the inbound request.&lt;/p&gt;

&lt;p&gt;When the request targets a Turbo Frame, we’ll respond with just the Turbo Frame we want to update, otherwise we’ll render the full page:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;k&quot;&gt;if&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;:query&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;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name like ?&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_request?&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;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;players: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@players&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;render&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;index&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This a bit a clunky (we’ll clean it up very soon!) but it demonstrates an important piece of how Turbo Frames work with Rails.&lt;/p&gt;

&lt;p&gt;We’re using the built-in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_request?&lt;/code&gt; &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb#L21&quot;&gt;helper&lt;/a&gt; to check for a specific &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-frame&lt;/code&gt; header in the request from the browser.&lt;/p&gt;

&lt;p&gt;When this header is present, our index action renders a partial that contains a matching Turbo Frame. Otherwise, we render the index page as normal.&lt;/p&gt;

&lt;p&gt;Finally, we need to update our search form to tell Turbo that we want to target the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; Turbo Frame we just added:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&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;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;GET&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;turbo_frame: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;players&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;c&quot;&gt;&amp;lt;!-- Snip the form content --&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-frame&lt;/code&gt; data attribute. When using Turbo Frames, we can add this data attribute &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#frame-with-overwritten-navigation-targets&quot;&gt;to tell Turbo&lt;/a&gt; that the response to the form submission (or link click) should target the given Turbo Frame.&lt;/p&gt;

&lt;p&gt;This extra effort is necessary because the search form and the list of players are not wrapped inside of a single Turbo Frame.&lt;/p&gt;

&lt;p&gt;When a form submission or link click occurs inside of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; element, Turbo will automatically attempts to retrieve and replace that frame’s content without any extra work from us.&lt;/p&gt;

&lt;p&gt;While we could try wrapping the form inside of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; Turbo Frame, doing so is undesirable. If a form is inside of a Turbo Frame, each time that frame is re-rendered, form inputs will lose their current values and any focus will be lost  — not an ideal user experience.&lt;/p&gt;

&lt;p&gt;Since we don’t want to wrap the form inside of the frame that contains our list of players, we need to explicitly tell Turbo that our form submission should update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; Turbo Frame.&lt;/p&gt;

&lt;h3 id=&quot;cleaning-up-the-frame-request&quot;&gt;Cleaning up the Frame request&lt;/h3&gt;
&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if turbo_frame_request?&lt;/code&gt; logic in the controller action is a bit messy. Thankfully we can clean this up a bit.&lt;/p&gt;

&lt;p&gt;First, create a new turbo_frame view:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/players/index.html+turbo_frame.erb&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that view in with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;players&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;@players&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt; to automatically look for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame&lt;/code&gt; views when a Turbo Frame request is received:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:turbo_frame_request_variant&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;turbo_frame_request_variant&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;variant&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:turbo_frame&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;turbo_frame_request?&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re using the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_request?&lt;/code&gt; helper to set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;request.variant&lt;/code&gt; on all inbound requests that contain a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-frame&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;With that update to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ApplicationController&lt;/code&gt;, we can simplify the index action in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; to:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;k&quot;&gt;if&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;:query&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;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name like ?&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;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;end&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Since the ApplicationController automatically sets the variant for us, individual controller actions no longer need to care about whether the request is for a Turbo Frame or not and Rails will invisibly render the right view for us.&lt;/p&gt;

&lt;p&gt;With these changes in place, refresh the page, type in a search query and see that the page content updates as before… so how do we know it is working?&lt;/p&gt;

&lt;p&gt;Check the server logs and see that when a form submission hits the server, Rails responds with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; partial content without re-rendering the application layout or the index view.&lt;/p&gt;

&lt;p&gt;If things are set up correctly, you should see something like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Processing by PlayersController#index as HTML
  Parameters: &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;query&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Dirk&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
   &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0.1ms&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  SELECT sqlite_version&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  ↳ app/controllers/players_controller.rb:7:in &lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;index&lt;span class=&quot;s1&quot;&gt;&apos;
  Rendering players/index.html+turbo_frame.erb
  Player Load (0.1ms)  SELECT &quot;players&quot;.* FROM &quot;players&quot; WHERE (name like &apos;&lt;/span&gt;%Dirk%&lt;span class=&quot;s1&quot;&gt;&apos;)
  ↳ app/views/players/index.html+turbo_frame.erb:2
  Rendered collection of players/_player.html.erb [1 times] (Duration: 0.2ms | Allocations: 65)
  Rendered players/index.html+turbo_frame.erb (Duration: 2.1ms | Allocations: 922)
Completed 200 OK in 5ms (Views: 2.4ms | ActiveRecord: 0.4ms | Allocations: 1903&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Notice the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rendering players/index.html+turbo_frame.erb&lt;/code&gt; in the logs which tells us everything is working as expected.&lt;/p&gt;

&lt;h3 id=&quot;updating-the-url-on-form-submission&quot;&gt;Updating the URL on form submission&lt;/h3&gt;
&lt;p&gt;A requirement for many search and filter experiences is updating the URL as the user makes selections so that they can bookmark or copy and paste a particular set of filters. While this isn’t really useful for our search form use case, it is important to know that Turbo makes it trivial to update the URL on a Turbo Frame request.&lt;/p&gt;

&lt;p&gt;To do this, we can update our form like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&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;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;GET&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;turbo_frame: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;advance&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;c&quot;&gt;&amp;lt;!-- Snip --&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;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo-action&lt;/code&gt; data attribute, giving it a value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance&lt;/code&gt;. This attribute tells Turbo to push the frame navigation into the browser’s history, updating the page’s URL in the process. This option is outlined in the Turbo documentation &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames#frame-that-promotes-navigations-to-visits&quot;&gt;here&lt;/a&gt; and the PR that added the functionality provides full details on how it works.&lt;/p&gt;

&lt;p&gt;Once this change is made, refresh the page, type in a search query and see that the URL updates in your browser. You can then copy/paste that URL into a new window and see that searching works during a full page visit as well (just remember to set the value of the input to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params[:query]&lt;/code&gt; value if you go this route).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-12-31-search-with-button.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing in a word to search a list of Dallas Mavericks basketball players and submit a form. When the form submits, the list of players updates&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Magical stuff.&lt;/p&gt;

&lt;h3 id=&quot;automatic-form-submission&quot;&gt;Automatic form submission&lt;/h3&gt;
&lt;p&gt;With our Turbo Frame powered form we already have a nice, efficient search experience in our application; however, it would be even better if the list of players could be updated as the user types.&lt;/p&gt;

&lt;p&gt;To do this, we’ll add a small Stimulus controller that automatically submits the form as the user types.&lt;/p&gt;

&lt;p&gt;To get started, generate the Stimulus controller with the built-in generator:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g stimulus form-submission&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then fill that controller (located at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/form_submission_controller.js&lt;/code&gt;) with the following:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;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;nc&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;nf&quot;&gt;search&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;clearTimeout&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;timeout&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;timeout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTimeout&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;requestSubmit&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;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;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; function waits for 200ms before calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit()&lt;/code&gt; on the element the controller is attached to in the DOM. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestSubmit()&lt;/code&gt; requires a polyfill to work on Safari but Turbo helpfully &lt;a href=&quot;https://github.com/hotwired/turbo/pull/439&quot;&gt;includes&lt;/a&gt; that polyfill for us.&lt;/p&gt;

&lt;p&gt;Next we’ll update the DOM to attach the Stimulus controller to the search form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&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;players_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;GET&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;controller: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;form-submission&quot;&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;s2&quot;&gt;&quot;players&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_action: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;advance&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Search by name:&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:query&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;input-&amp;gt;form-submission#search&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;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added two data attributes, one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller&lt;/code&gt; attribute to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element, connecting our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormSubmission&lt;/code&gt; controller. The other is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; attribute, telling Stimulus to call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form-submission#search&lt;/code&gt; on each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; event emitted by the form’s text field.&lt;/p&gt;

&lt;p&gt;With this in place, we can refresh the page and see that the form submits as we type, with a short delay to let the user pause typing before submitting the form.&lt;/p&gt;

&lt;p&gt;Here’s what the final experience looks like if you’ve been following along from start:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/2021-12-31-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing in a word to search a list of Dallas Mavericks basketball players. As they type, the list of players updates automatically.&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;This small example should help you get a taste for how simple it can be to build modern, highly-responsive applications with Ruby on Rails and the Hotwire stack, combining the feel of a SPA with the developer experience that the Rails world (usually) loves.&lt;/p&gt;

&lt;p&gt;To use this in the real world, we’d need to think about things like:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;An empty state: The list partial could render different content when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;players&lt;/code&gt; is empty&lt;/li&gt;
  &lt;li&gt;Cleaner, more performant database queries: Definitely don’t just leave your query sitting in the controller! For production use cases, you’d want to consider an option like &lt;a href=&quot;https://github.com/Casecommons/pg_search&quot;&gt;pg_search&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a deeper dive into Turbo Frames, you might enjoy &lt;a href=&quot;https://www.colby.so/posts/turbo-frames-on-rails&quot;&gt;my article&lt;/a&gt; dedicated to Turbo Frames. To learn about the other key component of Turbo, Turbo Streams, you can dig into my &lt;a href=&quot;https://www.colby.so/posts/turbo-streams-on-rails&quot;&gt;Turbo Streams on Rails&lt;/a&gt; article. To explore the full power of Turbo, the official &lt;a href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;Handbook&lt;/a&gt; and &lt;a href=&quot;https://turbo.hotwired.dev/reference/drive&quot;&gt;Reference&lt;/a&gt; docs are a good starting place.&lt;/p&gt;

&lt;p&gt;While this example is not yet production-ready, I hope it gives you a good starting point into the world of Turbo &amp;amp; Stimulus-powered Rails applications. The next time you’re building something new, consider whether Rails + the Hotwire Stack might be enough to deliver the experience your users expect while keeping your team happy and productive.&lt;/p&gt;

&lt;p&gt;As always, thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Handling modal forms with Rails, Tailwind CSS, and Hotwire</title>
   <link href="https://colby.so/posts/handling-modal-forms-with-rails-and-hotwire"/>
   <updated>2021-07-15T00:00:00+00:00</updated>
   <id>https://colby.so/posts/handling-modal-forms-with-rails-and-hotwire</id>
   <content type="html">&lt;p&gt;&lt;em&gt;September 2022 update: Since this article was written, Rails 7 and Turbo 7.2 have been released. There are new, simpler techniques to building modal forms with Turbo now. I published an &lt;a href=&quot;https://www.colby.so/posts/building-modal-forms-with-turbo-streams&quot;&gt;updated version&lt;/a&gt; of this article after the release of Turbo 7.2, I recommend reading that article instead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The release of the &lt;a href=&quot;https://hotwired.dev&quot;&gt;Hotwire&lt;/a&gt; package of Stimulus and Turbo has brought a tremendously powerful set of tools to Rails developers who want to bring the speed of a single page application to their users without sacrificing the development speed and happiness that Rails is known for.&lt;/p&gt;

&lt;p&gt;Today we are using Hotwire, Rails, and Tailwind CSS to build a modal form submission flow.&lt;/p&gt;

&lt;p&gt;With just a little bit of Stimulus and Turbo, we will create a user experience that opens a modal on click, retrieves modal content remotely, and cleanly handles successful form submission and form errors.&lt;/p&gt;

&lt;p&gt;This modal flow will help us learn a few of the core concepts of Hotwire while demonstrating how to implement a very common user experience in modern web development.&lt;/p&gt;

&lt;p&gt;The finished product will look something like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-15-finished.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a webpage clicking a link to create a comment, typing a message in the form that opens in a modal, and submitting the form successfully. After submission, the modal closes and the user&apos;s message appears in a list of messages on the page.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I am writing this guide assuming that you are comfortable with Ruby on Rails and that you have some level of familiarity with Stimulus. You do not need to be an expert on either.&lt;/p&gt;

&lt;p&gt;While we use Tailwind CSS to make our modal implementation simple, you do not need experience with Tailwind. If you prefer, you can implement your own, non-Tailwind modal and still get value from this guide.&lt;/p&gt;

&lt;p&gt;You can find the complete code for this project on &lt;a href=&quot;https://github.com/DavidColby/hotwire-modal-forms&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s get started.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;project-setup&quot;&gt;Project setup&lt;/h2&gt;

&lt;p&gt;We are going to start with a fresh Rails 6.1 application. If you prefer, you can follow along in an existing Rails 6 application, just be sure you’ve got Hotwire and Tailwind 2 installed.&lt;/p&gt;

&lt;p&gt;If you want to skip the setup steps and don’t have a repo handy, clone down &lt;a href=&quot;https://github.com/DavidColby/hotwire_tailwind_starter&quot;&gt;this repository&lt;/a&gt;, bundle install, and you’re good to go. If you take this route, jump ahead to the &lt;a href=&quot;#setting-up-comments&quot;&gt;setting up comments section&lt;/a&gt; below.&lt;/p&gt;

&lt;p&gt;To start from scratch, first open your terminal and run these commands:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new hotwire_modals &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;hotwire_modals
bundle add hotwire-rails
rails hotwire:install
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next, we will setup Tailwind CSS.&lt;/p&gt;

&lt;p&gt;In your terminal, run:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;yarn add tailwindcss@latest postcss@latest autoprefixer@latest @tailwindcss/forms
npx tailwind init
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update postcss.config.js:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;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;nf&quot;&gt;require&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;tailwindcss&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;./tailwind.config.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;nf&quot;&gt;require&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;postcss-import&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;nf&quot;&gt;require&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;postcss-flexbugs-fixes&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;nf&quot;&gt;require&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;postcss-preset-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;na&quot;&gt;autoprefixer&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;flexbox&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;no-2009&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;na&quot;&gt;stage&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And update tailwind.config.js:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;purge&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;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./app/**/*/*.html.erb&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;./app/helpers/**/*/*.rb&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;./app/javascript/**/*/*.js&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;na&quot;&gt;darkMode&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;c1&quot;&gt;// or &apos;media&apos; or &apos;class&apos;&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;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&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;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;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;na&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;nf&quot;&gt;require&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;@tailwindcss/forms&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next we’ll import Tailwind into the application. In your terminal run:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;app/javascript/stylesheets
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/javascript/stylesheets/application.scss
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And update the newly created application.scss file to import tailwind:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-scss&quot; data-lang=&quot;scss&quot;&gt;&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tailwindcss/base&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;s2&quot;&gt;&quot;tailwindcss/components&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;s2&quot;&gt;&quot;tailwindcss/utilities&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next, update your existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application.js&lt;/code&gt; file to import application.scss:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Rails&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/ujs&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@hotwired/turbo-rails&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ActiveStorage&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/activestorage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;channels&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// This is the line we&apos;re adding&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stylesheets/application&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;ActiveStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;controllers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;That’s a lot of copy and pasting, hang in there. One more to go before we get to the fun stuff.&lt;/p&gt;

&lt;p&gt;Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt; with the below:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&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;title&amp;gt;&lt;/span&gt;HotwireModals&lt;span class=&quot;nt&quot;&gt;&amp;lt;/title&amp;gt;&lt;/span&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;viewport&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width=device-width,initial-scale=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;csrf_meta_tags&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;csp_meta_tag&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;stylesheet_pack_tag&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;application&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;media: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;all&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;data-turbolinks-track&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;reload&apos;&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;javascript_pack_tag&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;application&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;data-turbolinks-track&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;reload&apos;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;body&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;max-w-7xl mx-auto mt-8&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;yield&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Perfect. Next we will scaffold up a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Comment&lt;/code&gt; resource, which we’ll use to build our modal form submission flow.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-comments&quot;&gt;Setting up comments&lt;/h2&gt;

&lt;p&gt;First, generate a Comment resource. In your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g scaffold Comment body:string
rails db:migrate
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/comments/_comment.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update the comments index view with a simple list view layout:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/view/comments/index.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;max-w-3xl mx-auto mt-8&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 justify-between items-baseline mb-6&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;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comments&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Comment&apos;&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;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;text-blue-600&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;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 items-baseline space-y-6 p-4 shadow-lg rounded&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;@comments&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;comment&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;render&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;ss&quot;&gt;comment: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;comment&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&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The index view loops through the comments and renders the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_comment&lt;/code&gt; partial for each one. Let’s fill that in with the below:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/view/comments/index.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;text-gray-700 border-b border-gray-200 w-full pb-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;n&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;/div&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;At this point, boot up your Rails server and head to &lt;a href=&quot;http://localhost:3000/comments&quot;&gt;http://localhost:3000/comments&lt;/a&gt;. You should see a layout that looks something like this.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-15-page-layout.png&quot; class=&quot;centered&quot; alt=&quot;A screen shot of a webpage with a list of comments displayed to the user. At the top right of the list, a link that reads new comment is displayed&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Go ahead and create a few comments to test things out.&lt;/p&gt;

&lt;p&gt;Once you confirm everything looks right, head on to the next section where we will add a modal to the replace the standalone New view that the scaffold generator provided.&lt;/p&gt;

&lt;h2 id=&quot;add-tailwind-stimulus-components&quot;&gt;Add Tailwind Stimulus Components&lt;/h2&gt;

&lt;p&gt;Rather than reinventing the wheel and writing a Stimulus controller to open and close our modal, we’re going to use &lt;a href=&quot;https://github.com/excid3/tailwindcss-stimulus-components&quot;&gt;Tailwind CSS Stimulus Components&lt;/a&gt;. This project provides easy-to-use, production-ready components for a variety of common front-end user interactions built with Stimulus and Tailwind, including a ready-to-use modal component.&lt;/p&gt;

&lt;p&gt;One of the joys of Ruby on Rails development is the incredibly robust ecosystem of community-built libraries that help reduce the effort that goes in to building Rails applications.&lt;/p&gt;

&lt;p&gt;While the Stimulus community is still growing, projects like Tailwind Stimulus Components are already making it easier to build Stimulus-powered frontend experiences.&lt;/p&gt;

&lt;p&gt;First add tailwind-stimulus-components to your application. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;yarn add tailwindcss-stimulus-components
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then import and register the Modal component in app/javascript/controllers/index.js:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;Modal&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;tailwindcss-stimulus-components&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;application&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;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;modal&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;nx&quot;&gt;Modal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;While we are going to use the basic behavior of the Modal component, for our purposes we’ll need to add a little bit of extra functionality to the modal controller. We can do this by extending the Modal controller.&lt;/p&gt;

&lt;p&gt;First, in your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/javascript/controllers/extended_modal_controller.js
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update the newly created controller as shown below:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;Modal&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;tailwindcss-stimulus-components&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;nc&quot;&gt;ExtendedModal&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modal&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;form&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;nf&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;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;p&quot;&gt;}&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;wire-up-the-modal&quot;&gt;Wire up the modal&lt;/h2&gt;

&lt;p&gt;With our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExtendedModal&lt;/code&gt; controller created, we are ready to start building our Hotwire-powered modal interface.&lt;/p&gt;

&lt;p&gt;First, create a partial to hold the modal content. From your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/comments/_modal.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill that partial in with the below:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-extended-modal-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;hidden animated fadeIn fixed inset-0 overflow-y-auto flex items-center justify-center&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;z-index: 9999;&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;max-w-lg max-h-screen w-full relative&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;m-1 bg-white rounded shadow&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;px-4 py-5 border-b border-gray-200 sm:px-6&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;h3&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-6 font-medium text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Comment
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/h3&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;form&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comment_form&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/form&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This partial contains the body of the modal.&lt;/p&gt;

&lt;p&gt;The style is mostly copied from the tailwind-stimulus-components documentation, the most important parts for our project are setting the data-extended-modal-target, which is required for the modal to open and the empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form id=&quot;comment_form&quot;&amp;gt;&lt;/code&gt; tag. This empty tag serves as a placeholder for the comments form content that will be populated when the modal opens.&lt;/p&gt;

&lt;p&gt;Next up, we’ll update the comments index view to connect our modal controller to the DOM:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- comments/index.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;s1&quot;&gt;&apos;comment_modal&apos;&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;max-w-3xl mx-auto mt-8&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 justify-between items-baseline mb-6&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;extended-modal&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-extended-modal-prevent-default-action-opening=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;false&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;render&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;modal&apos;&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comments&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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;New Comment&apos;&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;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;text-blue-600&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;click-&amp;gt;extended-modal#open&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;turbo-frame&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;comment_modal&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;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;s2&quot;&gt;&quot;comments&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;flex flex-col items-baseline space-y-6 p-4 shadow-lg rounded&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;vi&quot;&gt;@comments&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;comment&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;render&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;ss&quot;&gt;comment: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;comment&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we are into the meat of the project. Let’s walk through what we changed here.&lt;/p&gt;

&lt;p&gt;First, we render an empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo_frame&amp;gt;&lt;/code&gt;. We’ll populate this frame with the content of the modal when the New Comment link is clicked.&lt;/p&gt;

&lt;p&gt;Next, we connect our controller to the dom with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;extended-modal&quot;&lt;/code&gt; and override the default behavior of the modal controller with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prevent-default-action-opening=&quot;false&quot;&lt;/code&gt; which tells our modal controller to allow the link_to action to run as normal.&lt;/p&gt;

&lt;p&gt;The New Comment &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link_to&lt;/code&gt; which has two data attributes. The first is a standard Stimulus action attribute which fires the open function of our modal controller on click.&lt;/p&gt;

&lt;p&gt;The second is a Turbo attribute which tells Turbo to render the response from the server in the comment_modal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo_frame&amp;gt;&lt;/code&gt; that we added at the top of the view. Without this data attribute, Turbo would not know where to place the response from comments/new and clicking on the New Comment link would just render an empty modal.&lt;/p&gt;

&lt;p&gt;The basic flow of our modal is this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;User clicks on New Comment link&lt;/li&gt;
  &lt;li&gt;The click fires the extended-modal#open function which opens the modal&lt;/li&gt;
  &lt;li&gt;Because we told the open function to allow the default action to be followed, a request is sent to comments/new&lt;/li&gt;
  &lt;li&gt;When a response is received from comments/new, Turbo steps in and attempts to insert the response into the comment_modal turbo frame&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, we replace our comments wrapper div with a “comments” turbo_frame_tag. We will use this frame later to insert new comments after a successful form submission.&lt;/p&gt;

&lt;p&gt;Fancy stuff, but we’re not quite there yet.&lt;/p&gt;

&lt;p&gt;Next we need to update the new comments view and the comment form partial in order for the modal to properly render the comment form after it opens.&lt;/p&gt;

&lt;p&gt;Let’s start with updating the new view:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- comments/new.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;s1&quot;&gt;&apos;comment_modal&apos;&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;turbo-stream&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comment_form&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;replace&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;template&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;form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &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;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/turbo-stream&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re rendering a comment_modal turbo_frame tag and inside of that rendering a turbo-stream that looks for an element with an id of comment_form and replaces it.&lt;/p&gt;

&lt;p&gt;Next, the form partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;model: &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;id: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;comment_form&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;extended_modal_target: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;form&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;form&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;px-4 pt-5 pb-4 sm:p-6 sm:pb-4&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;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;p-4 border border-red-600&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;ul&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;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_messages&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;message&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;n&quot;&gt;message&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;/ul&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;form-group&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&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;/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;rounded-b mt-6 px-4 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;w-full sm:col-start-2 bg-blue-600 px-4 py-2 mb-4 text-white rounded-sm hover:bg-blue-700&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;button&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;extended-modal#close&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mt-3 w-full sm:mt-0 sm:col-start-1 mb-4 bg-gray-100 hover:bg-gray-200 rounded-sm px-4 py-2&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;/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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The form is almost entirely standard Rails form_with content. The important bits are passed in to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form_with&lt;/code&gt;. First we set the id of the form to comment_form. Recall that the turbo-stream in comments/new is looking for an element with that id.&lt;/p&gt;

&lt;p&gt;Next, we add a data-extended-modal-target attribute of “form”. We will use this target attribute later to reset the form after a successful submission.&lt;/p&gt;

&lt;p&gt;With those views in place, let’s update the comments controller to send turbo_stream responses when the posts#create action is called:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# app/controllers/comments_controller.rb&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Snipped content&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;@comment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Comment&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;comment_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;@comment&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;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;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;s1&quot;&gt;&apos;comments&apos;&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;s1&quot;&gt;&apos;comment&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &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;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# TODO: Handle errors&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;# Snipped content&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here’s where the turbo_stream magic happens.&lt;/p&gt;

&lt;p&gt;When our comment form is submitted, the create action is called and, if the comment saves, the server sends a turbo_stream response that appends the comment partial to a comments turbo_frame. Note that we do not need a comments.turbo_stream.erb partial, the existing html comments partial we have gets used automatically.&lt;/p&gt;

&lt;p&gt;The final step to properly handling our form submission is to close the modal after the form is submitted successfully. To do this, we will add a small function to the modal Stimulus controller and then attach an event listener to the DOM.&lt;/p&gt;

&lt;p&gt;First, add this function to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/extended_modal_controller.js&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;handleSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;detail&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;success&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;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;success&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;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;close&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;formTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reset&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;handleSuccess simply checks a boolean success value and, if success is true, we close the modal and reset the form. If success is false (meaning the form submission failed), our Stimulus controller does nothing.&lt;/p&gt;

&lt;p&gt;Finally, to ensure handleSuccess is called after the form is submitted, update the modal partial as shown:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-extended-modal-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;container&quot;&lt;/span&gt; 
     &lt;span class=&quot;na&quot;&gt;data-action=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;turbo:submit-end-&amp;gt;extended-modal#handleSuccess&quot;&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hidden animated fadeIn fixed inset-0 overflow-y-auto flex items-center justify-center&quot;&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;z-index: 9999;&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;!-- Snip inner 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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve added a data-action attribute listening for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo:submit-end&lt;/code&gt; &lt;a href=&quot;https://turbo.hotwired.dev/reference/events&quot;&gt;event&lt;/a&gt;. When that event fires, our modal controller’s handleSuccess function is called, allowing our modal to close when the form is submitted successfully.&lt;/p&gt;

&lt;p&gt;Whew. At this point, if you’ve been following along with each step, you should be able to load up the &lt;a href=&quot;http://localhost:3000/comments&quot;&gt;comments index page&lt;/a&gt;, click the New Comment link, type in a comment and submit it successfully. If everything is working, the new comment link will open a modal, and submitting the modal’s form will close the modal and append the new comment onto the comment list.&lt;/p&gt;

&lt;h2 id=&quot;handling-form-errors&quot;&gt;Handling form errors&lt;/h2&gt;

&lt;p&gt;To wrap up, we’ll update our CommentsController to handle errors. This’ll be quick.&lt;/p&gt;

&lt;p&gt;First, update the comments model to validate that body is present so that it is possible to have an invalid comment submission.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Comment&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_presence_of&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then head to the CommentsController and update the create method as shown:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;@comment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Comment&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;comment_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;@comment&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;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;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;s1&quot;&gt;&apos;comments&apos;&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;s1&quot;&gt;&apos;comment&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &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;p&quot;&gt;)&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;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;s1&quot;&gt;&apos;comment_form&apos;&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;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In the else block, we’re again rendering a turbo_stream response. This time, we target the comment_form turbo_frame and replace the content of frame instead of appending. We also send along a 422 status code so that the turbo:submit-end properly sets success to false. Otherwise, the default status code of 200 would cause our modal to close.&lt;/p&gt;

&lt;p&gt;With this error handling in place, submitting the modal form without typing anything into the body field will cause the form to rerender with the errors listed above the form, like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-15-error-handling.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user on a webpage clicking a link to create a comment, leaving the text input on the modal that opens blank and attempting to submit the form. The form updates with an error message informing the user that the input cannot be blank.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As our very last step, thanks to Turbo’s caching, an edge-case UI error can happen now that we have error handling in place.&lt;/p&gt;

&lt;p&gt;If a user attempts to submit the form with a blank body, and then corrects their error and successfully submits the form, clicking the New Comment link again will open the form with the error section still present on the modal.&lt;/p&gt;

&lt;p&gt;We can clear that up with a small addition to our modal Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;Modal&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;tailwindcss-stimulus-components&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;nc&quot;&gt;ExtendedModal&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Modal&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;form&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;errors&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;nf&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;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;nf&quot;&gt;handleSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;detail&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;success&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;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;success&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;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;close&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;nf&quot;&gt;clearErrors&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;formTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;reset&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;clearErrors&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;hasErrorsTarget&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;errorsTarget&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here the clearErrors function looks for the presence of errors on the form and removes them from the DOM if they exist. To make this work, update the form partial to add the data-extended-modal-target onto the errors container:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/comments/_form.html.erb --&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;@comment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;p-4 border border-red-600&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-extended-modal-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;errors&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&amp;gt;&lt;/span&gt;
      Could not save comment
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&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;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_messages&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;message&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;n&quot;&gt;message&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;/ul&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;That’s all for today. I hope that this guide has helped you see some of the power you can unlock by adding Hotwire to your Rails stack.&lt;/p&gt;

&lt;p&gt;With Hotwire, we were able to build a modal-based form submission flow that eliminates full page turns and delivers a modern-feeling, performant solution while still keeping our code easy to follow and build upon for any Rails developer.&lt;/p&gt;

&lt;p&gt;Hotwire is still new, and still evolving rapidly, in the future we can expect even more powerful functionality and even tighter integration with Rails as the community continues to add Hotwire to new and existing Rails applications.&lt;/p&gt;

&lt;p&gt;Here are a few resources I used as I wrote this guide:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/excid3/tailwindcss-stimulus-components&quot;&gt;Tailwind Stimulus Components&lt;/a&gt;: In addition to modals, this project gives you ready-to-use Alerts, Dropdowns, and Tabs, among other things. Before you roll your own component, check here.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://dev.to/dstull/how-to-use-modals-with-forms-in-rails-using-turbo-14n7&quot;&gt;How to use modals with forms in Rails using Turbo&lt;/a&gt;: This article served as a great starting point for my work and helped me reach the solution that I presented. Highly recommended if you’re interested in another approach to handling modal forms with Turbo&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://turbo.hotwired.dev/&quot;&gt;Turbo&lt;/a&gt; and &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; handbooks&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://github.com/hotwired/turbo&quot;&gt;Turbo source code&lt;/a&gt;: Turbo is evolving rapidly enough that even the official documentation can sometimes lag behind. Reading through the source can be a great way to learn more about the options you have for solving a problem, and for understanding what’s going on behind the scenes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;h3 id=&quot;about-the-author&quot;&gt;About the author:&lt;/h3&gt;

&lt;p&gt;I’m David - a solo founder and consultant. I write about software engineering, Ruby on Rails and the Rails-y ecosystem, product management, and the lessons I learn as I work to build sustainable SaaS businesses.&lt;/p&gt;

&lt;p&gt;My current project is &lt;a href=&quot;https://vestimonials.tv&quot;&gt;Vestimonials&lt;/a&gt;, an async video communication tool to help companies collect and share video testimonials from their employees and customers.&lt;/p&gt;

&lt;p&gt;I’m available for Rails, product management, and strategy consulting. Get in touch if you have questions about this article or anything else that’s on your mind.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a collapsible sidebar with Stimulus and Tailwind CSS</title>
   <link href="https://colby.so/posts/building-a-collapsible-sidebar-with-stimulus-and-tailwind"/>
   <updated>2021-07-06T00:00:00+00:00</updated>
   <id>https://colby.so/posts/building-a-collapsible-sidebar-with-stimulus-and-tailwind</id>
   <content type="html">&lt;p&gt;Today we’re building one of the most common elements in web design - a lefthand sidebar for navigation - using Tailwind CSS and Stimulus.js.&lt;/p&gt;

&lt;p&gt;Our sidebar will always take up 100% of the height of the page and we’ll be able to expand and collapse the sidebar by clicking a button. The whole thing will be accomplished with just a bit of Stimulus for the expand/collapse action and a small amount of Tailwind in our markup.&lt;/p&gt;

&lt;p&gt;Here’s what it will look like when we’re finished.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-06-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking an icon to collapse and expand a sidebar on a web page&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To accomplish this, we’ll start with a plain HTML file, pull in Tailwind CSS to make things look nice, and use Stimulus for interactivity.&lt;/p&gt;

&lt;p&gt;I’m writing this assuming a solid understanding of HTML and CSS, and some level of comfort with JavaScript.&lt;/p&gt;

&lt;p&gt;If you’ve never seen Tailwind before, some of the classes we add for styling might feel a little odd. You don’t need any knowledge of how Stimulus works, but if you’re brand new you might want to read the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus Handbook&lt;/a&gt; to help solidify some concepts as we go.&lt;/p&gt;

&lt;p&gt;You can find the complete code for this project on &lt;a href=&quot;https://gist.github.com/DavidColby/46b0684c7c166995830098596a3a192a&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s dive in.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;project-setup&quot;&gt;Project setup&lt;/h2&gt;

&lt;p&gt;For simplicity, we’re just going to use a plain old HTML file and pull in Tailwind and Stimulus from a CDN. In a real project, you should use a build system but we don’t need all that to demonstrate the concept!&lt;/p&gt;

&lt;p&gt;Let’s start with our plain HTML. Go ahead and copy and paste the below into a file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sidebar.html&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;lang=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;en&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;head&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&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;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;viewport&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width=device-width, initial-scale=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;title&amp;gt;&lt;/span&gt;Collapsible sidebar with Stimulus and Tailwind&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;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
      Here&apos;s our sidebar, eventually
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
      Here&apos;s where our main content lives
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’ll add in Stimulus and make Stimulus available through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.Stimulus&lt;/code&gt;. Add these script tags to the head tag, copied right from the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/installing#using-without-a-build-system&quot;&gt;Stimulus docs&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script &lt;/span&gt;&lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/stimulus/dist/stimulus.umd.js&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;application&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;application&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;sidebar&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;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&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;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;targets&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;&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;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then pull in Tailwind CSS from CDN, which is &lt;strong&gt;not recommended&lt;/strong&gt; for uses outside of demos like this. Tailwind has &lt;a href=&quot;https://tailwindcss.com/docs/installation&quot;&gt;extensive documentation&lt;/a&gt; for how to include Tailwind for just about any build system and framework you can imagine.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;stylesheet&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Perfect, now when we open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sidebar.html&lt;/code&gt; in our browser we should be able to access &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Stimulus&lt;/code&gt; in the JavaScript console and the defaults applied by Tailwind should be visible on our placeholder text.&lt;/p&gt;

&lt;p&gt;Let’s create the sidebar layout with Tailwind now.&lt;/p&gt;

&lt;h2 id=&quot;building-a-static-sidebar&quot;&gt;Building a static sidebar&lt;/h2&gt;

&lt;p&gt;Replace the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;body&amp;gt;&lt;/code&gt; of your sidebar.html file with the below to add the basic structure of the sidebar to your page.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;body&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;container&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&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;aside&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sm:w-1/5 bg-blue-500 min-h-screen&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;sticky top-0 pt-12 px-2 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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;absolute right-2 top-2 cursor-pointer&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-6 w-6&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=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M11 19l-7-7 7-7m8 14l-7-7 7-7&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;/div&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;ul&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 overflow-hidden 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;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1 inline-block&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6&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&amp;gt;&lt;/span&gt;
                    Home
                  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&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;hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9&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&amp;gt;&lt;/span&gt;
                    Learn
                  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&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;hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z&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&amp;gt;&lt;/span&gt;
                    About
                  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&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;flex items-center hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4&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&amp;gt;&lt;/span&gt;
                    Contact
                  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
                &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
              &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;main&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sm:w-4/5 p-4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        Main content goes here.
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/main&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;/body&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This method of adding a static sidebar uses flexbox to create two side-by-side containers, with the smaller content on the left and the content on the right. Using Tailwind, we don’t need to touch CSS at all to create the layout, instead relying entirely on existing Tailwind utility classes.&lt;/p&gt;

&lt;p&gt;Because we want the sidebar to have a background color that stretches (at minimum) the full height of the page, we add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min-h-screen&lt;/code&gt; class to the sidebar’s container. We want to keep the navigation links on the page at all times, even if the main content scrolls, so add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sticky&lt;/code&gt; class to the element that wraps the navigation menu.&lt;/p&gt;

&lt;p&gt;Our navigation links each have an icon. When we have our collapse/expand functionality in place, the collapsed sidebar will display the link icons without any text.&lt;/p&gt;

&lt;p&gt;With the body updated, refresh the page and see the basic layout in place. Clicking on the collapse/expand icon won’t do anything just yet.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-06-static-view.png&quot; class=&quot;centered&quot; alt=&quot;A screen shot of a web page with a blue sidebar displaying links and main content discussing global warming&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;creating-the-sidebar-stimulus-controller&quot;&gt;Creating the sidebar Stimulus controller&lt;/h2&gt;

&lt;p&gt;The Stimulus controller will handle collapsing and expanding the sidebar content when the user clicks the collapse/expand icon.&lt;/p&gt;

&lt;p&gt;First, we’ll add a function that toggles the sidebar from expanded to collapsed and back again. This function will look like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&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;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;sidebarContainerTarget&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;expanded&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;1&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;nf&quot;&gt;collapse&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;nf&quot;&gt;expand&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toggle()&lt;/code&gt; function uses a simple if statement that checks a data attribute in the DOM to determine if the sidebar is currently expanded or collapsed and then call the collapse or expand function as appropriate. We’ll create this data attribute in the next section.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;collapse()&lt;/code&gt; function is responsible for collapsing the expanded sidebar, and it looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;collapse&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;sidebarContainerTarget&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;nf&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;sm:w-1/5&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;iconTarget&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;`
  &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; class=&quot;h-6 w-6&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot;&amp;gt;
    &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M13 5l7 7-7 7M5 5l7 7-7 7&quot; /&amp;gt;
  &amp;lt;/svg&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;linkTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;link&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;link&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;nf&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;sr-only&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;sidebarContainerTarget&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;expanded&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;0&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we find the sidebar container, remove the width Tailwind class, swap out the icon for the expand icon, apply the Tailwind &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sr-only&lt;/code&gt; utility class to hide navigation link text from the page unless the user is accessing the site with a screenreader. Last, we update the sidebar’s expanded data attribute to 0, so that the next time the collapse/expand link is clicked the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toggle&lt;/code&gt; function calls the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expand&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;One of the wonderful things about Stimulus is that it provides a simple way to select elements from the DOM using &lt;a href=&quot;https://stimulus.hotwired.dev/reference/targets&quot;&gt;targets&lt;/a&gt;. These targets take the place of selecting elements by class or id, instead relying on data attributes.&lt;/p&gt;

&lt;p&gt;Using targets, we can keep our JavaScript and our HTML clear and readable, so that future developers don’t accidentally break our JavaScript by removing a class or changing an id.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expand()&lt;/code&gt; function comes next, and it looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;expand&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;sidebarContainerTarget&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;nf&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;sm:w-1/5&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;iconTarget&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;`
    &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; class=&quot;h-6 w-6&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot;&amp;gt;
      &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M11 19l-7-7 7-7m8 14l-7-7 7-7&quot; /&amp;gt;
    &amp;lt;/svg&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;linkTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;link&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;link&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;nf&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;sr-only&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;sidebarContainerTarget&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;expanded&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;1&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we simply reverse the changes our collapse function makes:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Setting the sidebar width&lt;/li&gt;
  &lt;li&gt;Swapping the icon&lt;/li&gt;
  &lt;li&gt;Display link text to all users&lt;/li&gt;
  &lt;li&gt;Set the expanded data attribute to 1 so the next click on the icon routes to collapse&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, we need to tell our Stimulus controller about the targets we’re using by updating the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targets&lt;/code&gt; definition at the top of the Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;targets&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;sidebarContainer&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;icon&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;link&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With these changes in place, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag that adds Stimulus to our page looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;application&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  
    &lt;span class=&quot;nx&quot;&gt;application&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;sidebar&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;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&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;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;targets&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;sidebarContainer&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;icon&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;link&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;nf&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;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;sidebarContainerTarget&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;expanded&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;1&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;nf&quot;&gt;collapse&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;nf&quot;&gt;expand&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;collapse&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;sidebarContainerTarget&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;nf&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;sm:w-1/5&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;sidebarContainerTarget&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;expanded&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;0&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;iconTarget&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;`
        &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; class=&quot;h-6 w-6&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot;&amp;gt;
          &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M13 5l7 7-7 7M5 5l7 7-7 7&quot; /&amp;gt;
        &amp;lt;/svg&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;linkTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;link&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;link&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;nf&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;sr-only&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;nf&quot;&gt;expand&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;sidebarContainerTarget&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;nf&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;sm:w-1/5&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;sidebarContainerTarget&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;expanded&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;1&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&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;iconTarget&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;`
          &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; class=&quot;h-6 w-6&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot;&amp;gt;
            &amp;lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M11 19l-7-7 7-7m8 14l-7-7 7-7&quot; /&amp;gt;
          &amp;lt;/svg&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;linkTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;link&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;link&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;nf&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;sr-only&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;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;If you make these updates, refresh the page and click on the collapse icon you’ll notice that absolutely nothing happens. That’s because our Stimulus controller isn’t connected to the DOM yet. We’ll finish up this guide by updating our HTML to make everything work.&lt;/p&gt;

&lt;h2 id=&quot;connecting-to-the-dom&quot;&gt;Connecting to the DOM&lt;/h2&gt;

&lt;p&gt;For our Stimulus controller to work, we first need to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;controller&lt;/code&gt; data attribute somewhere in the HTML. In general, you want to attach the controller to the parent element for the part of the DOM you plan to change within the controller. Since the Sidebar controller makes changes to the sidebar and the elements inside of the sidebar, it makes sense to attach the controller to the top-level sidebar element:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sidebar&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sm:w-1/5 bg-blue-500 min-h-screen&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;!-- Sidebar content --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next up, we need a data attribute to track the current state of the sidebar (expanded or collapsed):&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-sidebar-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sidebarContainer&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-expanded=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sm:w-1/5 bg-blue-500 min-h-screen&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;!-- Sidebar content --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The last update to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;aside&amp;gt;&lt;/code&gt; element is to tell our Stimulus controller that it is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sidebarContainer&lt;/code&gt; target:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;aside&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-sidebar-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sidebarContainer&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-expanded=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sidebar&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;sm:w-1/5 bg-blue-500 min-h-screen&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;!-- Sidebar content --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next up, our collapse icon container needs a target and an action:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;div&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;sidebar#toggle&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-sidebar-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;icon&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;absolute right-2 top-2 cursor-pointer&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-6 w-6&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=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;currentColor&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M11 19l-7-7 7-7m8 14l-7-7 7-7&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;/div&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; attribute is structured as “UserAction -&amp;gt; ControllerName#FunctionName”. For certain elements and actions, the UserAction component is optional but for a div, an action must always be provided. Read more about actions &lt;a href=&quot;https://stimulus.hotwired.dev/reference/actions&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With these attributes in place, each time the sidebar icon is clicked, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toggle()&lt;/code&gt; function defined in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sidebar&lt;/code&gt; controller will be called. If you refresh the page now you’ll notice an error in your JavaScript console. One last addition to the HTML and we’ll be all set:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;ul&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 overflow-hidden 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;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1 inline-block&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6&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;data-sidebar-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;link&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Home
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&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;hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9&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;data-sidebar-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;link&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Learn
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&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;hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z&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;data-sidebar-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;link&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          About
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&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;flex items-center hover:text-gray-200 h-8&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;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;#&quot;&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 h-8&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;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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-5 w-5 mr-1&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=&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;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;d=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4&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;data-sidebar-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;link&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
          Contact
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sidebar-target=&quot;link&quot;&lt;/code&gt; to each of the span’s that wrap the link text. Recall that in our Stimulus controller we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linkTargets&lt;/code&gt; to toggle the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sr-only&lt;/code&gt; class as needed. This ability to easily find and modify any number of elements without relying on classes that may or may not be changed in the future is one of the things that makes Stimulus so pleasant to work with.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;this.linkTargets.forEach(link =&amp;gt; {
  link.classList.add(&quot;sr-only&quot;)
})
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With that last change in place, refresh the page one last time and see that everything works as expected.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-06-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking an icon to collapse and expand a sidebar on a web page&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;Today we learned how to use Tailwind CSS and Stimulus to create a simple sidebar that users can collapse and expand on demand.&lt;/p&gt;

&lt;p&gt;While both a sidebar layout and collapsing/expanding content can be accomplished without any CSS or JavaScript frameworks, I hope this example has given you a taste for how readable, maintainable, and scalable Tailwind and Stimulus can be. A future developer revisiting code written with Stimulus and Tailwind can quickly deduce the purpose of each class and data attribute and can confidently make changes to implement new features to expand the site over time.&lt;/p&gt;

&lt;p&gt;We’re just scratching the surface of what Tailwind and Stimulus can do. To dig in further, start with:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind’s excellent (really, excellent) documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;The Stimulus Handbook&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.betterstimulus.com/&quot;&gt;Better Stimulus&lt;/a&gt; for Stimulus patterns and best practices&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;h3 id=&quot;about-the-author&quot;&gt;About the author:&lt;/h3&gt;

&lt;p&gt;I’m David - a solo founder and consultant. I write about software engineering, Ruby on Rails and the Rails-y ecosystem, product management, and the lessons I learn as I work to build sustainable SaaS businesses.&lt;/p&gt;

&lt;p&gt;My current project is &lt;a href=&quot;https://vestimonials.tv&quot;&gt;Vestimonials&lt;/a&gt;, an async video communication tool to help companies collect and share video testimonials from their employees and customers. I’m available for Rails, product management, and strategy consulting.  Get in touch if you have questions about this article or anything else that’s on your mind.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Remotely loading tabbed content with Ruby on Rails and Hotwire</title>
   <link href="https://colby.so/posts/remotely-loading-tab-content-with-rails-and-hotwire"/>
   <updated>2021-07-01T00:00:00+00:00</updated>
   <id>https://colby.so/posts/remotely-loading-tab-content-with-rails-and-hotwire</id>
   <content type="html">&lt;p&gt;&lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire&lt;/a&gt; is a set of tools for building web applications by sending HTML instead of JSON over the wire. Hotwire gives us a framework for making DOM updates without writing much, or any, JavaScript while delivering fast, modern-feeling web applications.&lt;/p&gt;

&lt;p&gt;In today’s example, we’re building a interface that remotely retrieves a portion of the page content from an endpoint and replaces a targeted portion of the DOM with the response from the endpoint. We’ll build this without writing any JavaScript and with only minor additions to the standard Rails code you already know how to write.&lt;/p&gt;

&lt;p&gt;Here is what it will look like when we are finished. We won’t be focused on styling today but our “tabs” will be fully functional and ready for you to add a nice looking Bootstrap or Tailwind skin.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-02-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user toggling between an Awards and Credits tab on a web page, with the content changing on each click&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To accomplish this, we will start with a new Rails 6.1 application, install Hotwire in the application, and then walk through the basics of adding &lt;a href=&quot;https://turbo.hotwired.dev/handbook/drive&quot;&gt;Turbo Drive&lt;/a&gt; to our views.&lt;/p&gt;

&lt;p&gt;I’m writing this assuming that you are comfortable with the basics of Ruby on Rails development and that you’ve never used Hotwire before.&lt;/p&gt;

&lt;p&gt;You can find the complete source code for this tutorial &lt;a href=&quot;https://github.com/DavidColby/hotwire-tabs&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s dive in.
&lt;!--more--&gt;&lt;/p&gt;

&lt;h2 id=&quot;set-up-our-project&quot;&gt;Set up our project&lt;/h2&gt;

&lt;p&gt;To get started, we’re going to create a new Rails application and create the resources we need to start implementing our remotely fetched tabs. The application will be centered around a Person. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; can have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Awards&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Credits&lt;/code&gt; and those awards and credits are what we’ll display in our tabs.&lt;/p&gt;

&lt;p&gt;Let’s get started with the following commands in our terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new hotwire_tabbing &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;hotwire_tabbing
rails g scaffold Person name:string
rails g model Credit name:string person:references
rails g model Award name:string person:references
rails db:migrate
rails s
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Visit &lt;a href=&quot;http://localhost:3000/people&quot;&gt;http://localhost:3000/people&lt;/a&gt; in your browser and create a person to use for testing and update the Person model to finish up the relationships with Awards and Credits.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&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;:awards&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:credits&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, in the Rails console (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails c&lt;/code&gt; in your terminal), create a couple of Awards and Credits with the Person you created in the UI. We won’t be covering creation of these resources in this lesson.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;no&quot;&gt;Person&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;nf&quot;&gt;awards&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Award&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;Best Director&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;Person&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;nf&quot;&gt;credits&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Credit&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;Star Wars - The good one&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now our application shell is all set up.&lt;/p&gt;

&lt;p&gt;Next up, we’ll install Hotwire and then we’ll start building.&lt;/p&gt;

&lt;h2 id=&quot;install-hotwire&quot;&gt;Install Hotwire&lt;/h2&gt;

&lt;p&gt;Add Hotwire to your Rails application by adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hotwire-rails&lt;/code&gt; to your Gemfile and then running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; from your terminal. If you prefer, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle add hotwire-rails&lt;/code&gt; from your terminal works too.&lt;/p&gt;

&lt;p&gt;Once Hotwire is added to your Gemfile, install Hotwire by running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails hotwire:install&lt;/code&gt; from your terminal. Hotwire is now installed and you’re ready to start building.&lt;/p&gt;

&lt;p&gt;Before moving on, &lt;strong&gt;be sure to restart your Rails server&lt;/strong&gt; or you’ll encounter some undefined method errors as you work through the rest of this guide.&lt;/p&gt;

&lt;h2 id=&quot;setup-awards-and-credits&quot;&gt;Setup awards and credits&lt;/h2&gt;

&lt;p&gt;Now that Hotwire is installed, our next step is to add controllers and views for Awards and Credits. These controllers and views will mostly look like standard Rails, with a few small adjustments to take advantage of the tools Hotwire provides.&lt;/p&gt;

&lt;p&gt;First generate the controllers in your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g controller Awards
rails g controller Credits
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And update the routes file to nest Awards and Credits endpoints under Person.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;:people&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;:awards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[index]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:credits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[index]&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next, let’s fill in our Awards controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AwardsController&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_person&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;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;p&quot;&gt;{&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;s1&quot;&gt;&apos;awards/list&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;awards: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;awards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;person: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&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;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_person&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Person&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;:person_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;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re using the person_id URL parameter to select the appropriate Person from the database, retrieiving that person’s Awards, and rendering a list partial that doesn’t yet exist.&lt;/p&gt;

&lt;p&gt;Note that while we’ve chosen to use a partial here since that feels more comfortable for me, this guide would work fine using a regular old index view instead of a partial.&lt;/p&gt;

&lt;p&gt;Since our list partial doesn’t exist yet, go ahead and create it now:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/awards/_list.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then fill it in:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;details_tab&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;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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;shared/tabs&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;span class=&quot;nt&quot;&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Awards won by &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&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;/h3&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;awards&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;award&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;n&quot;&gt;award&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;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;/ul&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Okay - now we’ve got a little bit of Hotwire code to review.&lt;/p&gt;

&lt;p&gt;Our entire list partial is wrapped in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;turbo_frame_tag&lt;/code&gt; which will turn into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; HTML element when this view renders. The &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb&quot;&gt;turbo frame helper&lt;/a&gt; provided by hotwire-rails requires an id argument (“details_tab” in our case) which Turbo uses to make DOM updates as needed.&lt;/p&gt;

&lt;p&gt;Besides the turbo_frame helper, the rest of this view is standard Rails.&lt;/p&gt;

&lt;p&gt;Note that the list partial relies on a shared/tabs partial that doesn’t exist yet. Create that now with:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;app/views/shared
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/shared/_tabs.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The tabs partial will render our tabs like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;Awards&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person_awards_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&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;br&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;s2&quot;&gt;&quot;Credits&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person_credits_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;These tabs are just simple HTML and we’ll render them in a couple of other places.&lt;/p&gt;

&lt;p&gt;With the Awards controller and view built out, let’s do the same thing for Credits before we finish up by updating the Person show page to tie everything together.&lt;/p&gt;

&lt;p&gt;First create the credits list partial.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/credits/_list.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And fill in the credits list partial with HTML that will look pretty familiar:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;s2&quot;&gt;&quot;details_tab&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;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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;shared/tabs&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;span class=&quot;nt&quot;&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Credits for &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&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;/h3&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;credits&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;credit&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;n&quot;&gt;credit&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;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;/ul&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update the Credits controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreditsController&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_person&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;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;p&quot;&gt;{&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;s1&quot;&gt;&apos;credits/list&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;credits: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;credits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;person: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&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;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_person&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Person&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;:person_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;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Perfect.&lt;/p&gt;

&lt;p&gt;Our last step is to update our Person show view to display the content of these tabs. Almost finished.&lt;/p&gt;

&lt;h2 id=&quot;tie-everything-together&quot;&gt;Tie everything together&lt;/h2&gt;

&lt;p&gt;The Person show page will be responsible for displaying the tabbed content that we built out through the Awards and Credits resources in the last section. Update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/people/show.html.erb&lt;/code&gt; like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;h2&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@person&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;/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;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;awards/list&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;person: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;awards: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;awards&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Notice that here we don’t have a single line of nonstandard Rails code here.&lt;/p&gt;

&lt;p&gt;The Show view simply renders the Person’s name and the awards list partial. The partial renders the tab navigation and the list of awards. All the heavy lifting is done behind the scenes by hotwire-rails by hooking into the details_tab &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; that our list partials render.&lt;/p&gt;

&lt;p&gt;In your browser, head to a show page for a Person with awards and credits and see that clicking the navigation links toggles the list content between Awards and Credits without requiring a full page turn.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/07-02-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user toggling between an Awards and Credits tab on a web page, with the content changing on each click&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Great work!&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;This simple technique for remotely loading tabbed content with Hotwire reveals a powerful way of improving application performance and end user experience with minimal additional engineering overhead.&lt;/p&gt;

&lt;p&gt;In a real application, moving requests for tabbed content that we don’t need right away into a separate request and eliminating the need for a full page turn reduces server load and speeds up page load times without major architecture changes. Small teams and solo developers can use Rails + Hotwire to provide modern, highly-performant web applications quickly and without getting into the heavy world of single page applications and heavy JavaScript frameworks.&lt;/p&gt;

&lt;p&gt;Further reading:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;For those new to Hotwire, the Turbo &lt;a href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;handbook&lt;/a&gt; is a great place to start&lt;/li&gt;
  &lt;li&gt;The other side of the Hotwire package is Stimulus, a modest JavaScript framework designed to play nicely with Turbo as you build client-side interactivity and more complex user interfaces with Hotwire. Check out the &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus handbook here&lt;/a&gt;. Stimulus is an incredibly powerful tool and is worth learning for any developer interested in adding client-side interactivity quickly.&lt;/li&gt;
  &lt;li&gt;If you’re working with Rails, the &lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;source code&lt;/a&gt; for turbo-rails is a great reference. We’re still in the early days of Turbo and you’ll discover neat options in the source that might not be well-documented in guides like this one yet&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Using Hotwire and Rails to build a live commenting system</title>
   <link href="https://colby.so/posts/using-hotwire-and-rails-to-build-a-commenting-system"/>
   <updated>2021-06-24T00:00:00+00:00</updated>
   <id>https://colby.so/posts/using-hotwire-and-rails-to-build-a-commenting-system</id>
   <content type="html">&lt;p&gt;Today we’re exploring &lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire&lt;/a&gt;, a new-old way of building web applications by sending HTML instead of JSON over the wire.&lt;/p&gt;

&lt;p&gt;We’ll learn how Hotwire works with Ruby on Rails by building a simple application that allows users to leave comments on a project and see those comments in real-time, without requiring full page turns.&lt;/p&gt;

&lt;p&gt;This guide presents an alternative approach to &lt;a href=&quot;https://www.colby.so/posts/building-a-simple-commenting-form-with-stimulus-js-and-rails-6&quot;&gt;a guide&lt;/a&gt; I wrote last year which guides you through creating a similar experience using Stimulus. Either approach is valid and works well. The approach presented today presents a way of delivering the experience without writing any JavaScript code, and should feel very natural for any Ruby on Rails developer.&lt;/p&gt;

&lt;p&gt;Here is what it will look like when we are finished. It won’t be pretty, but it will work and we won’t write a single line of JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/06-24-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing in a comment in a text input and the comment being added to a list of comments&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To accomplish this, we will start with a new Rails 6.1 application, install Hotwire in the application, and then walk through the basics of adding Hotwire to our views and controllers.&lt;/p&gt;

&lt;p&gt;I’m writing this assuming that you are comfortable with the basics of Ruby on Rails development and that you’ve never used Hotwire before.&lt;/p&gt;

&lt;p&gt;You can find the complete source code for this tutorial &lt;a href=&quot;https://github.com/DavidColby/hotwire-comments-demo&quot;&gt;on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s dive in.
&lt;!--more--&gt;&lt;/p&gt;

&lt;h2 id=&quot;set-up-our-project&quot;&gt;Set up our project&lt;/h2&gt;

&lt;p&gt;To get started we will create a new Rails application and scaffold up a Project resource to build our commenting system against. While you can work from an existing application of your own, you will find this guide easier to follow if you start from scratch along with me.&lt;/p&gt;

&lt;p&gt;First, run these console commands to create and setup our Rails app:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails new hotwire-comments &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;hotwire-comments
rails g scaffold Project name:string
rails g model Comment body:text project:references
rails db:migrate
rails s
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In your browser, head to &lt;a href=&quot;http://localhost:3000/projects&quot;&gt;http://localhost:3000/projects&lt;/a&gt; and create a project. We’ll use this project as we build the commenting system.&lt;/p&gt;

&lt;p&gt;Once that’s done, open the application’s code in your favorite editor and add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;has_many :comments&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/models/project.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, display comments on the project show page by updating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/projects/show.html.erb&lt;/code&gt; with the following:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/projects/show.html.erb --&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;notice&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;notice&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&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Name:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;/p&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;vi&quot;&gt;@project&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;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;comment&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&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;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;/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;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Edit&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_project_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@project&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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Back&apos;&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And add a few comments in the rail console:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;Project.first.comments &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Comment&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;.create(body: &quot;Hotwire comments are coming soon!&quot;)
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Wonderful.&lt;/p&gt;

&lt;p&gt;If you’ve followed along successfully so far, when you visit &lt;a href=&quot;http://localhost:3000/projects/1&quot;&gt;http://localhost:3000/projects/1&lt;/a&gt; you should see something that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/06-24-initial-comments.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of a page that shows the name of a project and a comment entered by a user&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now we’re ready to install Hotwire and start learning.&lt;/p&gt;

&lt;h2 id=&quot;installing-hotwire&quot;&gt;Installing Hotwire&lt;/h2&gt;

&lt;p&gt;To install Hotwire, first add the hotwire-rails gem to your Gemfile by running this commend in your console or by manually updating your Gemfile and running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; in your terminal.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;bundle add hotwire-rails
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Once the gem is added, run the installer from your terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails hotwire:install
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;After the installer runs, restart your rails server or you may encounter some undefined method errors later in this guide.&lt;/p&gt;

&lt;p&gt;Now that we have Hotwire installed, let’s start with getting comments to render in real time.&lt;/p&gt;

&lt;h2 id=&quot;adding-a-comments-stream&quot;&gt;Adding a comments stream&lt;/h2&gt;

&lt;p&gt;Hotwire as we’re using it relies on frameworks designed and maintained by the Basecamp team. One of those frameworks is &lt;a href=&quot;https://turbo.hotwired.dev/&quot;&gt;Turbo&lt;/a&gt;. Turbo is a set of “complimentary techniques for speeding up page changes and form submissions”.&lt;/p&gt;

&lt;p&gt;For our purposes, we’ll be interested in two key components of the Turbo package, &lt;a href=&quot;https://turbo.hotwired.dev/reference/frames&quot;&gt;Turbo Frames&lt;/a&gt; and &lt;a href=&quot;https://turbo.hotwired.dev/reference/streams&quot;&gt;Turbo Streams&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A Turbo Stream delivers page changes to the browser over Websocket. By adding the Hotwire Rails project to our application, we gain access to a set of helper methods that make working with Turbo Streams simple.&lt;/p&gt;

&lt;p&gt;Let’s see how this works by updating our project show page as follows.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/projects/show.html.erb --&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;notice&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;notice&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&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Name:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;/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;turbo_stream_from&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;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;id=&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;vi&quot;&gt;@project&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;_comments&quot;&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;created_at: :desc&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Edit&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_project_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@project&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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Back&apos;&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Let’s walk through the changes here one-by-one.&lt;/p&gt;

&lt;p&gt;First, we add a turbostream that listens for broadcasts on the comments channel for our Project. This stream is how we subscribe to comment updates for our project. Note that because we’re working with a nested resource here (comments belong to a project), our stream subscribes to comments for just the current project.&lt;/p&gt;

&lt;p&gt;We also add an id to the parent div of our comments list. This id is used to identify where to add broadcasted comments to the DOM. If this id is not present or does not match the id in the broadcast, no DOM updates will occur when comment changes are broadcast.&lt;/p&gt;

&lt;p&gt;Finally, we’re using a _comment partial to render each comment. That partial doesn’t exist yet, let’s add it now:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;app/views/comments
&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/comments/_comment.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then fill in the comment partial with the below content.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/comments/_comment.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;id=&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;n&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;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;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;/div&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note that here, we’re ensuring each comment has a unique id in the DOM, which ensures that new comments are properly inserted into the DOM.&lt;/p&gt;

&lt;p&gt;Our last step towards getting our stream setup and comments prepending in real time is to add a callback to the comment model.&lt;/p&gt;

&lt;p&gt;When this callback runs, the newly created comment will be broadcast on the project_comments stream that our project#show page is now subscribed to.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;  &lt;span class=&quot;c1&quot;&gt;# app/models/comment.rb&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;n&quot;&gt;after_create_commit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;broadcast_prepend_to&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project&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;target: &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;dom_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project&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;_comments&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we are using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_prepend_to&lt;/code&gt; method provided by turbo-rails. There are a variety of methods that can be used to add, remove, and replace DOM content through callbacks triggered by model changes. The best place to find these methods is by reading the &lt;a href=&quot;https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb&quot;&gt;turbo-rails code&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Note that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt; value passed to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;broadcast_prepend_to&lt;/code&gt; must exactly match the id set in the DOM earlier.&lt;/p&gt;

&lt;p&gt;The dom_id method used to set the broadcast channel target is not normally available in models so we include the relevant ActionView helper class in the model. This simplifies our method of broadcasting to the appropriate channel. H/T to Chris Oliver at &lt;a href=&quot;https://gorails.com/&quot;&gt;GoRails&lt;/a&gt; for this helpful trick.&lt;/p&gt;

&lt;p&gt;With these changes in place, let’s test out our stream and see how it works.&lt;/p&gt;

&lt;p&gt;Make sure you’ve got your project open in a web browser, and then open your Rails console and create a comment. If you’ve followed along successfully, immediately after you create the comment in the console, you should see the comment added to the project page open in your browser, like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/06-24-through-the-console.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing in a command in their terminal to add a comment. Next to the terminal, the user&apos;s web browser is open and the comment is added to the browser window after the command is run in the terminal&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;adding-comments-with-a-form&quot;&gt;Adding comments with a form&lt;/h2&gt;

&lt;p&gt;While our subscription is working great, users aren’t going to add content through the Rails console. Let’s finish up this tutorial by adding a form to add comments in the UI without requiring a page turn.&lt;/p&gt;

&lt;p&gt;First, let’s create a partial to render our form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;app/views/comments/_form.html.erb
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And add the below to the form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/comments/_form.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;s2&quot;&gt;&quot;comment_form&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;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;model: &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;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project_comments_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@project&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;form&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;k&quot;&gt;if&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;errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;any?&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error_explanation&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&amp;gt;&lt;/span&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluralize&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;errors&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error&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; prohibited this comment from being saved:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

        &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&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;errors&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;error&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;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full_message&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;/ul&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;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This is pretty close to a standard Rails form partial, with the added turbo_frame_tag that wraps the entire &amp;lt;form&amp;gt; element.&lt;/p&gt;

&lt;p&gt;This frame_tag allows us to selectively replace the content of the form partial on form submission without writing any special Javascript.&lt;/p&gt;

&lt;p&gt;Now we’ll need a controller to handle submissions from this form.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;rails g controller Comments
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Fill in the controller with the below content:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;c1&quot;&gt;# app/controllers/comments_controller.rb&lt;/span&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;@project&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Project&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;:project_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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;new&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;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;k&quot;&gt;if&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;save&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;turbo_stream&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;s1&quot;&gt;&apos;comment_form&apos;&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;s1&quot;&gt;&apos;comments/form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Comment&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;p&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;p&quot;&gt;{&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;s1&quot;&gt;&apos;comments/form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Comment&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;k&quot;&gt;else&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;turbo_stream&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;s1&quot;&gt;&apos;comment_form&apos;&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;s1&quot;&gt;&apos;comments/form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &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;p&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;p&quot;&gt;{&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;s1&quot;&gt;&apos;comments/form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &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;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;comment_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;:comment&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;:body&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our comments controller create method is a fairly standard Rails controller with one twist: the create method responds to turbo_stream requests. When a turbo_stream request is sent to this endpoint, the controller responds with a turbo_stream response to replace the comment_form DOM element with the comments/form partial, both on a successful request and a failed request. Note that, as before, the “comment_form” id in our turbo_frame needs to match the target passed to the turbo_stream.replace method.&lt;/p&gt;

&lt;p&gt;The only difference between the failure and success responses is using the existing @comment on failed responses so that errors can be rendered. On success, we set the comment local variable for the form partial to a new comment to clear out the body field on the form.&lt;/p&gt;

&lt;p&gt;To test out the error path, you can update your Comment model to validate the presence of the body field.&lt;/p&gt;

&lt;p&gt;Next, before we can create comments, we need to add the appropriate route to the routes.rb file:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;:projects&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;:comments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;%i[create]&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, let’s display the comment form on the project show page:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/projects/show.html.erb --&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;notice&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;notice&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&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Name:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;/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;comments/form&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;comment: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Comment&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;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_from&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;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;id=&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;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&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;vi&quot;&gt;@project&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;_comments&quot;&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;render&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;created_at: :desc&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Edit&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit_project_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@project&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;link_to&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Back&apos;&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Refresh your project page after you update the view.&lt;/p&gt;

&lt;p&gt;If everything is set up correctly, you should see new comments added without a page turn. If you submit with the comment body left blank, you should see error messages added to the page.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/06-24-finished-project.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing in a comment in a text input and the comment being added to a list of comments&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;Combining Hotwire and Rails, with a sprinkling of Stimulus for client-side interactivity is a powerful way to build performant, scalable, developer-and-user friendly web applications.&lt;/p&gt;

&lt;p&gt;While single page applications and independent frontends built with the latest JavaScript framework have their place, if you’re a small team building standard SaaS applications, I encourage you to spend time learning about Hotwire-powered applications to keep your team productive and focused on solving problems for your users, not fighting with your tech stack.&lt;/p&gt;

&lt;p&gt;Helpful Hotwire resources:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://hotwired.dev/&quot;&gt;Hotwire intro video&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;Turbo handbook&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus handbook&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gorails.com/episodes/hotwire-rails&quot;&gt;GoRails Hotwire introduction&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/hotwired/turbo-rails&quot;&gt;turbo-rails source&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thanks for reading! As always, get in touch if you have any questions or if I can be of any help.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a horizontal slider with Stimulus and Tailwind CSS</title>
   <link href="https://colby.so/posts/building-a-horizontal-slider-with-stimulus-and-tailwind"/>
   <updated>2021-05-06T00:00:00+00:00</updated>
   <id>https://colby.so/posts/building-a-horizontal-slider-with-stimulus-and-tailwind</id>
   <content type="html">&lt;p&gt;Today we’re building a component that is common but deceptively tricky to get right - a horizontal slider with a position indicator and navigation buttons.&lt;/p&gt;

&lt;p&gt;We’ll have a list of items of an arbitrary length, and our slider will allow folks to scroll to see every item in the list. As they scroll, indicators below the slider will update to show which items are visible on the screen. Clicking on the indicators will scroll the corresponding item into view. The whole thing is pretty fancy.&lt;/p&gt;

&lt;p&gt;Here’s what it will look like when we’re finished.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/05-06-completed-slider.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user scrolling a web page horizontally across an image gallery with identical images of red shoes&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To accomplish this, we’ll start with a plain HTML file, pull in Tailwind CSS to make things look nice, and use Stimulus to build interactivity for our position indicators and navigation buttons.&lt;/p&gt;

&lt;p&gt;I’m writing this assuming a solid understanding of HTML and CSS, and some level of comfort with JavaScript. If you’ve never seen Tailwind before, some of the classes we add for styling might feel a little odd. You don’t need any knowledge of how Stimulus works, but if you’re brand new you might want to read the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus Handbook&lt;/a&gt; to help solidify some concepts.&lt;/p&gt;

&lt;p&gt;Let’s dive in.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;project-setup&quot;&gt;Project setup&lt;/h2&gt;

&lt;p&gt;For simplicity, we’re just going to use a plain old HTML file and pull in Tailwind and Stimulus from a CDN. In a real project, you should probably use a build system but we don’t need all that to demonstrate the concept!&lt;/p&gt;

&lt;p&gt;Let’s start with our plain HTML. Go ahead and copy and paste the below into a file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slider.html&lt;/code&gt; or use a more exciting name. You’re the boss.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;lang=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;en&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;head&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;charset=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;utf-8&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;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;viewport&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width=device-width, initial-scale=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;title&amp;gt;&lt;/span&gt;Horizontal slider with Stimulus and Tailwind&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;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
      Here&apos;s where our slider will go, momentarily.
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’ll add in Stimulus and make Stimulus available through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.Stimulus&lt;/code&gt;. Add these script tags to the head tag, copied right from the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/installing#using-without-a-build-system&quot;&gt;Stimulus docs&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;src&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;https://unpkg.com/stimulus/dist/stimulus.umd.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/script&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&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;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;application&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;application&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;slider&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;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&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;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;targets&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;&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;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/script&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then pull in Tailwind CSS from CDN, which is &lt;strong&gt;not recommended&lt;/strong&gt; for uses outside of demos like this. Tailwind has &lt;a href=&quot;https://tailwindcss.com/docs/installation&quot;&gt;extensive documentation&lt;/a&gt; for how to include Tailwind for just about any build system and framework you can imagine.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;link&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;rel=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;stylesheet&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Perfect, now when we open our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slider.html&lt;/code&gt; we should be able to access &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.Stimulus&lt;/code&gt; in the JavaScript console and the defaults applied by Tailwind should be visible on our placeholder text.&lt;/p&gt;

&lt;p&gt;Let’s build the slider with Tailwind now.&lt;/p&gt;

&lt;h2 id=&quot;create-our-horizontal-slider&quot;&gt;Create our horizontal slider&lt;/h2&gt;

&lt;p&gt;We’ll start with the basic structure of the slider, with no Tailwind classes, and then we’ll add in the Tailwind classes to make everything function. Replace the text in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;main&amp;gt;&lt;/code&gt; with the HTML below.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;container&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;h1&amp;gt;&lt;/span&gt;Our slider&apos;s title&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;scrolling-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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Open up &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slider.html&lt;/code&gt; and you’ll see some giant pictures of shoes. Not quite what we want, but a good starting point.&lt;/p&gt;

&lt;p&gt;We’ll start with a flex container to hold our slider’s header, which will be static, and the slider itself, which will scroll horizontally. Update the content of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;main&amp;gt;&lt;/code&gt; to include some basic container classes.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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;container&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 my-24&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;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900 text-center mb-4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Our slider&apos;s title&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;scrolling-content&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex overflow-x-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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-96 h-64 px-4 flex-shrink-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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The really important changes here are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flex overflow-x-scroll&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrolling-content&lt;/code&gt; div. That sets the div to flex the child divs and adds the horizontal scrolling behavior we’re looking for with the CSS property &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;overflow-x: scroll&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flex-shrink-0&lt;/code&gt; to the individual image divs. This ensures the image divs don’t resize themselves to fit the viewport width using the CSS property &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flex-shrink: 0&lt;/code&gt;. Without this, the image divs would shrink automatically and the overflow-x-scroll property on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrolling-content&lt;/code&gt; div wouldn’t do anything useful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, we’ve got a simple scrolling image gallery, nice work! Now we’ll get into JavaScript land by adding indicators that show the user which images are currently on screen and that function as navigation buttons to scroll the content to the clicked indicator.&lt;/p&gt;

&lt;h2 id=&quot;add-navigation-indicators&quot;&gt;Add navigation indicators&lt;/h2&gt;

&lt;p&gt;Our indicators will be circles that change color based on whether they are in the active viewport or not. Again, we’ll start with our HTML. Add this HTML to the bottom of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;container&lt;/code&gt; div.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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 mx-auto my-8&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;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex justify-center&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;!-- Note that we have one &amp;lt;li&amp;gt; for each image in our gallery --&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’ve got some nice looking circles below our scrolling images, but they don’t serve any purpose. Next up is creating a Stimulus controller to make the dots come to life.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/05-06-static-dots.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of an image gallery of identical red shoes, with gray dots below&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;bring-the-indicators-to-life-with-stimulus&quot;&gt;Bring the indicators to life with Stimulus&lt;/h2&gt;

&lt;p&gt;The Stimulus controller will be responsible for two things:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Updating the color of the indicator circles based on whether or not the corresponding image is currently visible to the user&lt;/li&gt;
  &lt;li&gt;Handling clicks on indicators and scrolling the container to the corresponding image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first task, we’ll rely on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API&quot;&gt;the IntersectionObserver API&lt;/a&gt;. This API is well-supported across modern browsers and is commonly used for tasks like lazy-loading images. In our case, we’re going to use it to change the color of the indicator circles. Let’s get started.&lt;/p&gt;

&lt;p&gt;Update the Stimulus controller currently defined in our head with the following:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&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;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;application&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  
    &lt;span class=&quot;nx&quot;&gt;application&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;slider&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;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Stimulus&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;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;targets&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;scrollContainer&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;image&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;indicator&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;nf&quot;&gt;initialize&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;observer&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;nc&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;onIntersectionObserved&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;scrollContainerTarget&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.5&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;imageTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;image&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;observer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;observe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;image&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;onIntersectionObserved&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entries&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;entries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entry&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;if &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intersectionRatio&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.5&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;intersectingIndex&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;imageTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entry&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;indicatorTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intersectingIndex&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;nf&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;bg-blue-900&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;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;intersectingIndex&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;imageTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;indexOf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;entry&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;indicatorTargets&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;intersectingIndex&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;nf&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;bg-blue-900&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;span class=&quot;p&quot;&gt;})()&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt;/script&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;gt;
&lt;/span&gt;  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There’s a lot here, let’s break it down a bit.&lt;/p&gt;

&lt;p&gt;First, we add a few &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targets&lt;/code&gt; to our controller. We’ll use these to reference DOM elements that our controller cares about.&lt;/p&gt;

&lt;p&gt;In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initialize&lt;/code&gt; method, we create a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;observer&lt;/code&gt; using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IntersectionObserver&lt;/code&gt; constructor. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onIntersectionObserved&lt;/code&gt; callback function passed to the constructor is the function that will be called each time a visibility threshold is crossed.&lt;/p&gt;

&lt;p&gt;In (closer-to) human terms: as you scroll the images left or right, the observer watches the visible part of the screen and fires the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onIntersectionObserver&lt;/code&gt; function each time an image is more (or less) than half visible on the screen.&lt;/p&gt;

&lt;p&gt;Also note that we bind &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onIntersectionObserved&lt;/code&gt; function so that we can reference &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this&lt;/code&gt; and get back our Stimulus controller inside of the onIntersectionObserved function. Without binding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;this&lt;/code&gt; we would not be able to use Stimulus targets in this function and our JavaScript would be a bit more complicated.&lt;/p&gt;

&lt;p&gt;At the end of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initialize&lt;/code&gt; method, we tell our observer which DOM elements it should watch over.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;onIntersectionObserved&lt;/code&gt; function simply loops over all of the watched DOM elements and adds a class if the element is more than half visible or removes that class if the element is not.&lt;/p&gt;

&lt;p&gt;With this JavaScript added, refresh &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slider.html&lt;/code&gt; and see that nothing happens. To make this work, we need to update the HTML to connect the Stimulus controller to the DOM.&lt;/p&gt;

&lt;p&gt;Let’s update our HTML as follows:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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 my-24&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;slider&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;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900 text-center mb-4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Our slider&apos;s title&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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex overflow-x-scroll&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;scrollContainer&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&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 mx-auto my-8&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;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex justify-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;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The changes here are:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;We added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;slider&quot;&lt;/code&gt; to our wrapper div to tell Stimulus that this div should be tied to our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SliderController&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;We added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slider-target=&quot;scrollContainer&quot;&lt;/code&gt; to the div that wraps our images and scrolls on the x-axis.&lt;/li&gt;
  &lt;li&gt;We added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slider-target=&quot;image&quot;&lt;/code&gt; to each of the image divs.&lt;/li&gt;
  &lt;li&gt;We added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-slider-target=&quot;indicator&quot;&lt;/code&gt; to each of the indicator &amp;lt;li&amp;gt; tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The addition of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;slider&quot;&lt;/code&gt; is mandatory - without adding this declaration our Stimulus code will never be executed. The targets are all technically optional and you could accomplish the same by adding classes or ids to the DOM but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targets&lt;/code&gt; are a super helpful way to keep your code clean and concise and if you’re using Stimulus you should be using targets to reference DOM elements in most cases.&lt;/p&gt;

&lt;p&gt;If you refresh &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slider.html&lt;/code&gt; again, you’ll see that the circles change color as we slide images in and out of view. Resize the browser, get wild with it if you want. One more step to go.&lt;/p&gt;

&lt;h2 id=&quot;add-onclick-navigation&quot;&gt;Add onClick navigation&lt;/h2&gt;

&lt;p&gt;Now that we’ve got these nice navigation circles, the last step is to allow users to navigate between images by clicking on the corresponding circle. This can be accomplished with a new method in our Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// Add this function alongside the existing initialize and onIntersectionObserved functions&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;scrollTo&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;imageId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&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;imageId&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;imageElement&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;nf&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;imageId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;imageElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scrollIntoView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;block&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;end&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;na&quot;&gt;inline&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;nearest&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;na&quot;&gt;behavior&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;smooth&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This new function starts by identifying the target image and then uses &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView&quot;&gt;Element.scrollIntoView()&lt;/a&gt; to scroll the parent container into the viewport, if it is not already visible.&lt;/p&gt;

&lt;p&gt;To make this work, we need to add appropriate attributes to the images and indicators HTML, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&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 my-24&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;slider&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;h1&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-3xl text-gray-900 text-center mb-4&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Our slider&apos;s title&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;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex overflow-x-scroll&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;scrollContainer&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;3&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;5&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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;w-96 h-64 px-4 flex-shrink-0&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;image&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;6&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;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://images.unsplash.com/photo-1542291026-7eec264c27ff?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c2hvZXN8ZW58MHx8MHx8&amp;amp;ixlib=rb-1.2.1&amp;amp;auto=format&amp;amp;fit=crop&amp;amp;w=900&amp;amp;q=60&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&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 mx-auto my-8&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;ul&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;flex justify-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;li&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-image-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1&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;slider#scrollTo&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-image-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;2&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;slider#scrollTo&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-image-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;3&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;slider#scrollTo&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-image-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;4&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;slider#scrollTo&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-image-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;5&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;slider#scrollTo&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&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;h-6 w-6 rounded-full mx-2 cursor-pointer bg-gray-500&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-slider-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;indicator&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-image-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;6&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;slider#scrollTo&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/ul&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note the changes here. Each image container div is given an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; and each indicator is given a corresponding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-image-id&lt;/code&gt;. In the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollTo&lt;/code&gt; function, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-image-id&lt;/code&gt; in a vanilla JavaScript &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.getElementById&lt;/code&gt; call. The assigned ids are arbitrary - you could give each image a name or use a random string, as long as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;image-id&lt;/code&gt; data attribute on the indicator matches the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt; on the image, you’re good to go.&lt;/p&gt;

&lt;p&gt;After adding the ids, we also added data-actions to each indicator. The data-action attribute tells Stimulus which function to call when the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;click&lt;/code&gt; action occurs on the element. For more details on how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; works, the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus Handbook&lt;/a&gt; is a great starting point!&lt;/p&gt;

&lt;p&gt;Refresh the page one more time and click on a circle for an image that isn’t on screen and your browser should scroll until that image is visible. Magic!&lt;/p&gt;

&lt;h2 id=&quot;improving-scrollto&quot;&gt;Improving scrollTo&lt;/h2&gt;

&lt;p&gt;While our scrollTo method works fine in isolation right now, if our slider element isn’t the only thing on the page, folks will have a fairly jarring experience - clicking on a dot will scroll the page horizontally (good!) and vertically (weird!).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/05-06-vertical-jump.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on a circle below an image gallery of red shoes and the browser window scrolling up and left rapidly&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This happens because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollIntoView&lt;/code&gt; assumes you need to scroll both horizontally and vertically. You can’t only scroll horizontally with this function. This works great for full screen experiences where your slider is the only content on the page (like a full screen image gallery) but it fails when your slider has other content above and below it (like a gallery of product images on an ecommerce store listing)&lt;/p&gt;

&lt;p&gt;To workaround this limitation, we can replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollIntoView&lt;/code&gt; with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo&quot;&gt;scrollTo&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollTo&lt;/code&gt; allows us to scroll an element to a given x and y coordinate pair but, crucially, you can choose to only provide an x coordinate, eliminating any weird vertical scrolling.&lt;/p&gt;

&lt;p&gt;Let’s update our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollTo&lt;/code&gt; Stimulus function to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollTo&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollIntoView&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;scrollTo&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;imageId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&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;imageId&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;imageElement&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;nf&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;imageId&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;imageCoordinates&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;imageElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;getBoundingClientRect&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;scrollContainerTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scrollTo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;left&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;scrollContainerTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scrollLeft&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;imageCoordinates&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;top&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;na&quot;&gt;behavior&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;smooth&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our new function has two key changes:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;First, we extract the current position of our image relative to the viewport with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect&quot;&gt;getBoundingClientRect&lt;/a&gt;. This function returns, among other things, the x and y position of the element.&lt;/li&gt;
  &lt;li&gt;Next, we replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollIntoView&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollTo&lt;/code&gt;. In the options, we set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;top&lt;/code&gt; to false to indicate we don’t want to change the vertical scroll and set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; to the current left scroll position of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollContainer&lt;/code&gt; + the the image’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; (or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt;) position. Combining the current scroll position and the target element’s x position allows us to reliably scroll the container left and right programatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this update in place, navigating the scroll container by clicking on the indicator circles no longer causes vertical scrolling.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/05-06-no-jumping.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking on a circle below an image gallery of red shoes and the browser not scrolling up, only left&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;bonus-round-scroll-behavior-improvements&quot;&gt;Bonus round: Scroll behavior improvements&lt;/h2&gt;

&lt;p&gt;To finish up, let’s add a few more CSS rules to our slider to make it look and feel a little nicer.&lt;/p&gt;

&lt;p&gt;First, we can add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hide-scroll-bar&lt;/code&gt; class to our scroll container. This built-in Tailwind CSS class hides the scroll bar, which looks a bit nicer and isn’t necessary with our indicators in place.&lt;/p&gt;

&lt;p&gt;Next, we can prevent unwanted back navigation on mobile devices by adding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;overscroll-x-contain&lt;/code&gt; class to the scroll container. Another built-in Tailwind class, this stops overscrolling in the scroll container (like swiping too aggressively to the left) from triggering scrolling on the whole page.&lt;/p&gt;

&lt;p&gt;Finally, we’ll step outside of Tailwind for some scroll behavior CSS rules. Add a style tag to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;head&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slider.html&lt;/code&gt; and add the following CSS:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-css&quot; data-lang=&quot;css&quot;&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;style&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&quot;text/css&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nc&quot;&gt;.gallery-item&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;scroll-snap-align&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start&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;nc&quot;&gt;.gallery&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nl&quot;&gt;-webkit-overflow-scrolling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;touch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;scroll-snap-type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mandatory&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;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;These rules instruct the browser to snap scrolling to each element with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type&quot;&gt;scroll-snap-type&lt;/a&gt;, adds momentum based scrolling on touch devices with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-overflow-scrolling&quot;&gt;-webkit-overflow-scrolling&lt;/a&gt; and tells the browser where to snap to for each gallery item with &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-align&quot;&gt;scroll-snap-align&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Add the gallery class to the scroll container and gallery-item to each image div and notice that scrolling the container now nicely snaps to each element when scrolling finishes.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/05-06-completed-slider.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user scrolling a web page horizontally across an image gallery with identical images of red shoes&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up-and-further-reading&quot;&gt;Wrapping up and further reading&lt;/h2&gt;

&lt;p&gt;Some caveats to note before you use this code in production: at the time of this writing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;intersectionObserver&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollTo&lt;/code&gt; are not implemented on IE11 and Safari does not support &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scrollTo&lt;/code&gt; options. You may wish to adjust the scrollTo function call to not pass in options or add polyfills for support on IE11, depending on your needs.&lt;/p&gt;

&lt;p&gt;Special thanks goes to &lt;a href=&quot;https://dev.to/joostkiens&quot;&gt;Joost Kiens&lt;/a&gt; who wrote an &lt;a href=&quot;https://dev.to/joostkiens/creating-practical-instagram-like-galleries-and-horizontal-lists-with-css-scroll-snapping-580e&quot;&gt;excellent article&lt;/a&gt; on using the intersectionObserver API to build an single element scroll container that served as a base for what we built today.&lt;/p&gt;

&lt;p&gt;You can find the complete code for this guide on &lt;a href=&quot;https://gist.github.com/DavidColby/3d31c47fc0345a49f50774540d2231dc&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For questions or comments, you can find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want to learn more about Tailwind or Stimulus, the official documentation for both is a great place to start. In particular, Tailwind’s documentation is some of the best on the internet and is highly recommended if you want to learn more about how Tailwind works.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;The Stimulus Handbook&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://tailwindcss.com/docs&quot;&gt;Tailwind docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a video converter with Rails 6 and FFmpeg</title>
   <link href="https://colby.so/posts/building-a-video-converter-with-rails-and-ffmpeg"/>
   <updated>2021-04-28T00:00:00+00:00</updated>
   <id>https://colby.so/posts/building-a-video-converter-with-rails-and-ffmpeg</id>
   <content type="html">&lt;p&gt;Today’s project pulls together a number of built-in Rails tools, plus a couple of incredibly useful open source projects to build a web application that can convert user-uploaded video files to MP4.&lt;/p&gt;

&lt;p&gt;To demonstrate some of the modern parts of the standard Rail stack, we’re going to go further than just building a converter. We will enhance the user experience during the upload and conversion process with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Active Storage&lt;/code&gt; to handle direct file uploads, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Action Cable&lt;/code&gt; for real-time updates on the video conversion, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Stimulus&lt;/code&gt; to dynamically update the DOM without needing a heavy Javascript framework.
&lt;!--more--&gt;
When we’re finished, we’ll have an ugly but fully functional web application that allows a user to upload a video file. After the file is uploaded, we’ll convert the video to MP4 if needed, and then display that converted video to the user. It will look something like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/04-28-final-converter.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking a submit button, seeing a progress bar, and then progress text that counts up as a video converts. When finished, the text is replaced with the video element&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This guide assumes that you are comfortable with Ruby on Rails and familiar with &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_storage_overview.html&quot;&gt;Active Storage&lt;/a&gt; and &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt;, but you do not need to be an expert on any of these tools to get value from this guide. We’ll start with a brand new Rails 6 project, however, you can follow along with an existing Rails 6 project if you prefer.&lt;/p&gt;

&lt;p&gt;If you use an existing project you may need to complete additional setup steps for Stimulus, Action Cable, and Active Storage that won’t be covered in this guide.&lt;/p&gt;

&lt;h3 id=&quot;project-setup&quot;&gt;Project setup&lt;/h3&gt;

&lt;p&gt;To start, let’s create our Rails application using webpack and Stimulus, install Active Storage, add a User scaffold to work from, and migrate our database.&lt;/p&gt;

&lt;p&gt;All standard Rails stuff here, hopefully nothing totally new yet!&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;    rails new upload_progress &lt;span class=&quot;nt&quot;&gt;--webpack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;stimulus &lt;span class=&quot;nt&quot;&gt;--skip-coffee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--database&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;postgresql &lt;span class=&quot;nt&quot;&gt;-T&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;upload_progress
    rails db:create
    rails g scaffold User name:string
    rails active_storage:install
    rails db:migrate
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;At this point, you can boot your rails server with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails s&lt;/code&gt; and visit &lt;a href=&quot;http://localhost:3000/users&quot; target=&quot;_blank&quot;&gt;localhost:3000/users&lt;/a&gt; to see the scaffold working as expected. Create a few users if you want. There are no rules here.&lt;/p&gt;

&lt;p&gt;Now that the project is setup, let’s get into the fun stuff.&lt;/p&gt;

&lt;h3 id=&quot;uploading-files-with-active-storage&quot;&gt;Uploading files with Active Storage&lt;/h3&gt;

&lt;p&gt;Our user will have a profile video, which we’ll use as the base for exploring video conversion later on in this guide.&lt;/p&gt;

&lt;p&gt;To add a profile video, update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; model to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;profile_video&lt;/code&gt; backed by Active Storage.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/models/user.rb&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;User&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_one_attached&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:profile_video&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Thanks to the power of Active Storage we can now just add the profile video to our controller and views and start uploading files right away.&lt;/p&gt;

&lt;p&gt;Head to the users form and add a new form field for the video:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/users/_form.html.erb --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;model:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;err&quot;&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;user.errors.any&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;err&quot;&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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;error_explanation&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&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;pluralize&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;user.errors.count&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;)&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt; prohibited this user from being saved:&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

          &lt;span class=&quot;nt&quot;&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;user.errors.each&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;err&quot;&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;li&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;error.full_message&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;err&quot;&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;/ul&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;err&quot;&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;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;field&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.label&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:name&lt;/span&gt; &lt;span class=&quot;err&quot;&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.text_field&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:name&lt;/span&gt; &lt;span class=&quot;err&quot;&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;/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;field&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.label&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:profile_video&lt;/span&gt; &lt;span class=&quot;err&quot;&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.file_field&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:profile_video&lt;/span&gt; &lt;span class=&quot;err&quot;&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;/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;actions&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.submit&lt;/span&gt; &lt;span class=&quot;err&quot;&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;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UsersController&lt;/code&gt; to add profile_video to the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_params&lt;/code&gt; method.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/controllers/users_controller.rb&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;user_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;:user&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;:profile_video&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;/figure&gt;

    &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

  &lt;/div&gt;

&lt;p&gt;Finally, we want to be able to see the video after we’ve uploaded it. Head to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/users/show&lt;/code&gt; and add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element to the page.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/users/show.html.erb --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;video&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;controls&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;source&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;%= url_for(@user.profile_video) %&amp;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;/video&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Try it out and make sure that uploading videos works as expected by creating a new user and attaching a video file to the profile_video field.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/04-28-video-profile-page.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot showing a video displayed on a plain white page&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Stunning. You’re a star.&lt;/p&gt;

&lt;p&gt;While this works, we can make the upload experience a little better.&lt;/p&gt;

&lt;p&gt;First - when the file upload dialog opens, the user can choose any type of file they like. Let’s add a guardrail to only allow video files to be selected in the upload dialog.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/users/_form_.html.erb --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.file_field&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:profile_video&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;accept:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;video&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Note that in a real app &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;accept&lt;/code&gt; is a nice user experience gain but it does not validate the user’s input. You should always validate the file type on the server! A malicious or curious user can easily change the accept attribute in the browser. For today we can get by with field-level validation on the client.&lt;/p&gt;

&lt;h3 id=&quot;adding-direct-uploads-and-an-upload-progress-bar&quot;&gt;Adding direct uploads and an upload progress bar&lt;/h3&gt;

&lt;p&gt;When a user uploads a large file directly to your servers, they are going to find the request takes a long time and, in production, it may frequently time out, block other requests, and cause all kinds of headaches.&lt;/p&gt;

&lt;p&gt;We can prevent all of those issues with the &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_storage_overview.html#direct-uploads&quot;&gt;direct upload&lt;/a&gt; feature built in to Active Storage. Direct upload sends files directly from the user’s client to your chosen cloud storage provider. Direct uploads speed up the upload and keeping the request from timing out or backing up your servers.&lt;/p&gt;

&lt;p&gt;Let’s add direct upload to the file field and, as a bonus, add an upload progress bar for the user.&lt;/p&gt;

&lt;p&gt;First, confirm that you have ActiveStorage’s javascript added to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application.js&lt;/code&gt; file.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;// application.js&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// If you started with a new Rails project, these lines should already be in application.js. If not, add them&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;ActiveStorage&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/activestorage&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;ActiveStorage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now, we need to tell our file field to use the direct upload javascript:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/users/_form_.html.erb --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.file_field&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:profile_video&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;accept:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;video&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;direct_upload:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Refresh your page and confirm that everything works exactly as it did before we added direct upload. Again, Rails makes things really easy.&lt;/p&gt;

&lt;p&gt;While our file upload is working and we could leave it as it is, in the real world uploads can take a while, especially when you’re handling large video files. Giving the user feedback as their file uploads is a nice UX win. Let’s add a progress bar to track the file upload using a few sprinkles of &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To track upload progress, we’ll listen for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direct-upload:progress&lt;/code&gt; event and, each time that event is emitted we’ll use the data in the event to update a progress bar in the UI.&lt;/p&gt;

&lt;p&gt;We’ll start with our Stimulus controller, which looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/upload_progress_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;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;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;nc&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;progress&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;progressText&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;progressWidth&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;nf&quot;&gt;initialize&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;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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;direct-upload:progress&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;updateProgress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;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;nf&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;direct-upload:error&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;event&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;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;preventDefault&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;detail&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;nf&quot;&gt;log&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;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;nf&quot;&gt;showProgress&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;progressTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;display&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;block&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;nf&quot;&gt;updateProgress&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;progress&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;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;detail&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;progressWidthTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;width&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;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;progress&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;progressTextTarget&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;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;progress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;% complete`&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;nf&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;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;removeEventListener&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;direct-upload:progress&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;updateProgress&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Let’s break this down a little bit - I’m assuming you’ve got a little bit of familiarity with Stimulus here, if you don’t, the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus Handbook&lt;/a&gt; is a great starting point.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;connect&lt;/code&gt; is used to setup our event listener and subscribe to the events we care about - in this case, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direct-upload:progress&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direct-upload:error&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Our progress event listener calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updateProgress&lt;/code&gt; and then uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;progress&lt;/code&gt; data from the event to update the UI elements that make up our progress bar.&lt;/p&gt;

&lt;p&gt;Next we’ll add our progress bar HTML and connect our Stimulus controller to the DOM.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/users/_form_.html.erb --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form_with&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;model:&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;html:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;controller:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;upload-progress&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;

      &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- snipped form fields --&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;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;display: none;&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-upload-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progress&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;Uploading your video to our servers: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressText&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-upload-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressText&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Warming up...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;progress-bar&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;background: gray; position: relative; width: 200px; height: 20px;&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressWidth&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-upload-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressWidth&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width: 0%; height: 100%; position: absolute; background: green;&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&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;actions&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;form.submit&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Save&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;action:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;click-&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;upload-progress#showProgress&quot; } %&amp;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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There’s a lot here, let’s step through the updates individually.&lt;/p&gt;

&lt;p&gt;First, we connect our Stimulus controller to the DOM by adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data: { controller: “upload-progress” }&lt;/code&gt; to our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form_with&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then, we add our progress bar HTML. Don’t mind all the inline styles, you can use CSS if you like.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;display: none;&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-upload-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progress&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;Uploading your video to our servers: &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressText&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-upload-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressText&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Warming up...&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&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;progress-bar&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;background: gray; position: relative; width: 200px; height: 20px;&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;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressWidth&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-upload-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressWidth&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;width: 0%; height: 100%; position: absolute; background: green;&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&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The progress bar is hidden when the page loads and we use Stimulus targets on our progress bar container, progress text, and progress width elements. The Stimulus controller uses these targets to update the DOM as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;direct-upload:progress&lt;/code&gt; events are emitted.&lt;/p&gt;

&lt;p&gt;Finally, on the form’s submit button, we listen for a click. When the form submit button is clicked, we call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upload-progress#showProgress&lt;/code&gt; to remove the hidden style from the progress bar container.&lt;/p&gt;

&lt;h3 id=&quot;adding-the-video-converter-service&quot;&gt;Adding the video converter service&lt;/h3&gt;

&lt;p&gt;Now we’ve got our profile videos uploading nicely and we are informing users of the progress throughout.&lt;/p&gt;

&lt;p&gt;Great progress, but we are accepting and displaying any video type and our goal is to build a video converter so that we have a predictable video type on profile videos. For this tutorial, let’s say that our requirements are that all uploaded videos must end up as MP4s.&lt;/p&gt;

&lt;p&gt;Before we dive in to the details, let’s review what we want to accomplish at a high level. We want to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Allow a user to upload a video file through the User Profile form&lt;/li&gt;
  &lt;li&gt;When a video file is uploaded, check the video’s content type&lt;/li&gt;
  &lt;li&gt;If the video is already an mp4 file, we don’t need to convert the video - our work is done&lt;/li&gt;
  &lt;li&gt;If the video is not an mp4 file, we want to transcode the video from its original type to .mp4&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, how do we accomplish step four? Converting a video on the fly sounds complicated, right?&lt;/p&gt;

&lt;p&gt;Enter &lt;a href=&quot;https://www.ffmpeg.org/&quot;&gt;FFmpeg&lt;/a&gt;, an incredibly popular, open source solution for working with video (and audio, if that’s your thing).&lt;/p&gt;

&lt;p&gt;While we can work with FFmpeg directly, we’re going to use a gem to make our interaction with FFmpeg a little simpler: &lt;a href=&quot;https://github.com/streamio/streamio-ffmpeg&quot;&gt;streamio-ffmpeg&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s start by adding the gem to our project with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle add streamio-ffmpeg&lt;/code&gt; and installing ffmpeg on our system with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;brew install ffmpeg&lt;/code&gt; on a Mac. Other installation options can be found &lt;a href=&quot;https://ffmpeg.org/download.html&quot;&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With FFmpeg ready, let’s add a service to handle video conversion with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir app/services &amp;amp;&amp;amp; touch app/services/video_converter.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here’s what the service looks like:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/services/video_converter.rb&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VideoConverter&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_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;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;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;convert!&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;process_video&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;process_video&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;profile_video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;tmpdir: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;/tmp&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;movie&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;FFMPEG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Movie&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;file&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;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tmp/video-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SecureRandom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;alphanumeric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12&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;.mp4&quot;&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;movie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transcode&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;video_codec: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;libx264&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;audio_codec: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;aac&apos;&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;profile_video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;attach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;io: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;open&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;ss&quot;&gt;filename: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;video-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SecureRandom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;alphanumeric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12&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;.mp4&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;content_type: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;video/mp4&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;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The important part of the service is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;process_video&lt;/code&gt; method, so let’s zoom in there.&lt;/p&gt;

&lt;p&gt;First, we open the existing profile video attached to the user, so we can access the video’s path.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@user.profile_video.open(tmpdir: &quot;/tmp&quot;) do |file|&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, we create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Movie&lt;/code&gt; object with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;streamio-ffmpeg&lt;/code&gt; gem, using the original file uploaded by the user, which we’re going to transcode shortly.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;movie = FFMPEG::Movie.new(file.path)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The path variable assignment is the location where we will create the new, transcoded video. Using this new path and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Movie&lt;/code&gt; object, we call the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transcode&lt;/code&gt; &lt;a href=&quot;https://github.com/streamio/streamio-ffmpeg/blob/master/lib/ffmpeg/movie.rb#L202&quot;&gt;method&lt;/a&gt; from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;streamio-ffmpeg&lt;/code&gt; gem.&lt;/p&gt;

&lt;p&gt;Finally, we attach the newly created video to the user, replacing the video that was previously uploaded. Easy!&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@user.profile_video.attach(io: File.open(path), filename: &quot;video-#{SecureRandom.alphanumeric(12)}.mp4&quot;, content_type: &apos;video/mp4&apos;)&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&quot;adding-a-background-job&quot;&gt;Adding a background job&lt;/h3&gt;

&lt;p&gt;One more step before we can start converting uploaded videos. Working with video can be time and resource intensive. We don’t want to convert a video during a page turn or block our application servers with expensive video processing.&lt;/p&gt;

&lt;p&gt;Let’s add a job that we can use to enqueue video processing jobs in the background. For this tutorial, we’ll use ActiveJob with the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:async&lt;/code&gt; adapter, but in production you’ll want to use a real background processor.&lt;/p&gt;

&lt;p&gt;Add the job with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g job convert_video&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Our job receives a user id and calls the video converter service for that user, like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ConvertVideoJob&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_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;no&quot;&gt;VideoConverter&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;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;convert!&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h3 id=&quot;converting-uploaded-videos&quot;&gt;Converting uploaded videos&lt;/h3&gt;

&lt;p&gt;Now we’re ready to convert videos. In our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UsersController&lt;/code&gt;, we’ll enqueue the background job in our create and update methods, after the user is saved.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/controllers/users_controller.rb&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;@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;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;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;save&lt;/span&gt;
          &lt;span class=&quot;no&quot;&gt;ConvertVideoJob&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;@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;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# snip response boilerplate&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;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update&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;k&quot;&gt;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;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;no&quot;&gt;ConvertVideoJob&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;@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;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# snip response boilerplate&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now when we upload a profile video, our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VideoConverter&lt;/code&gt; service will convert the video to mp4 and replace the uploaded video with the newly converted video.&lt;/p&gt;

&lt;p&gt;Try it out by uploading any non-mp4 video from the users form. In your server logs you should see output that looks like this if everything is working as expected:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INFO -- : Transcoding of /tmp/ActiveStorage-22-20210428-10611-1wvew4g.mov to tmp/video-9ZrvVFnAZZTJ.mp4 succeeded&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;But wait a minute. Transcoding can take a while and it happens in the background, so when I upload a video, the show page will render the non-transcoded video until after transcoding is complete. That’s going to cause all kinds of weird bugs, right?&lt;/p&gt;

&lt;p&gt;Right.&lt;/p&gt;

&lt;p&gt;Let’s get fancy with &lt;a href=&quot;https://guides.rubyonrails.org/action_cable_overview.html&quot;&gt;Action Cable&lt;/a&gt;, plus a few more sprinkles of Stimulus to track video conversion progress and render the converted video without a page turn.&lt;/p&gt;

&lt;h3 id=&quot;using-action-cable-to-broadcast-video-updates&quot;&gt;Using Action Cable to broadcast video updates&lt;/h3&gt;

&lt;p&gt;First let’s talk through the problem again at a high level. We want to accomplish something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a video is uploaded, check to see if it is an mp4&lt;/li&gt;
&lt;li&gt;If it isn’t, flag the video as needs conversion&lt;/li&gt;
&lt;li&gt;If the video is flagged as needs conversion, don’t render the video element on the user’s show page&lt;/li&gt;
&lt;li&gt;If a video is being processed, communicate that to the user&lt;/li&gt;
&lt;li&gt;If the video has been processed, or did not need to be processed, render the video element on the show page&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s start with the logic we skipped in the last section to check if the video needs to be transcoded. Since there’s no point in converting an mp4 to an mp4, we should check the uploaded video’s content type before doing any processing.&lt;/p&gt;

&lt;p&gt;If the content type is video/mp4, then we don’t need to transcode the video. If the content type is anything else, flag the video and start converting.&lt;/p&gt;

&lt;p&gt;Before we write the logic code, let’s add a boolean value to our User model to track whether the profile video needs to be converted:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g migration AddConvertVideoToUsers convert_video:boolean&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Migrate your database before moving on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails db:migrate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now we can add the logic to our controller. In the real world, the controller probably isn’t the right place for this code, but we’re here to learn, so let’s stay focused.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/controllers/users_controller.rb&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;@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;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;k&quot;&gt;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;save&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;update_conversion_value&lt;/span&gt;
          &lt;span class=&quot;no&quot;&gt;ConvertVideoJob&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;@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;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# snip render logic&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;c1&quot;&gt;# PATCH/PUT /users/1 or /users/1.json&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;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;k&quot;&gt;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;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;update_conversion_value&lt;/span&gt;
          &lt;span class=&quot;no&quot;&gt;ConvertVideoJob&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;@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;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# snip render logic&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;kp&quot;&gt;private&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# snip&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update_conversion_value&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;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;profile_video&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;needs_conversion&lt;/span&gt; &lt;span class=&quot;o&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;profile_video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;video/mp4&quot;&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;update_column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:convert_video&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;needs_conversion&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The slightly clunky &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_conversion_value&lt;/code&gt; method checks the content type of the profile video and assigns the appropriate value to the convert_video column.&lt;/p&gt;

&lt;p&gt;Now that we’ve got this logic in place, we can head into our converter service and put that logic to use.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/services/video_converter.rb&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;convert!&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;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;convert_video?&lt;/span&gt;
      
      &lt;span class=&quot;n&quot;&gt;process_video&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This change ensures that our video converter doesn’t convert the video unless it has been flagged for conversion.&lt;/p&gt;

&lt;p&gt;Next we want to update the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;convert_video&lt;/code&gt; after we’ve converted non-mp4 videos. We can do that with a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_needs_conversion&lt;/code&gt; method in our service:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/services/video_converter.rb&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VideoConverter&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;convert!&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;@user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;convert_video?&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;process_video&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;update_needs_conversion&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;c1&quot;&gt;# Snip&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;update_needs_conversion&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;update_column&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:convert_video&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now when we upload a video, the converter will return without doing anything if the video is an mp4, otherwise it will convert the video and then update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;convert_video&lt;/code&gt; flag on the user to false.&lt;/p&gt;

&lt;p&gt;With this flag in place, we can add in our Action Cable and Stimulus magic to communicate a video’s conversion status to the user so they can see their uploaded video without refreshing the page.&lt;/p&gt;

&lt;p&gt;Let’s remember our goal for this part of the project. We want to:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Check the video’s conversion status when we render the user show page&lt;/li&gt;
  &lt;li&gt;If the video is converted, display the video in a video element&lt;/li&gt;
  &lt;li&gt;If the video is not converted, display a placeholder for the video and communicate the video’s conversion status to the user&lt;/li&gt;
  &lt;li&gt;When the video finishes converting, automatically update the content of the user’s show page to display the video in a video element&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s dig in.&lt;/p&gt;

&lt;p&gt;First, we need to generate an Action Cable channel to broadcast from. As usual, Rails comes with a built in converter for this. Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g channel VideoConversion&lt;/code&gt; to generate the new channel&lt;/p&gt;

&lt;p&gt;Update the generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;video_conversion_channel.rb&lt;/code&gt; file to look like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;VideoConversionChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationCable&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;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;subscribed&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;stream_from&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;video_conversion_&lt;/span&gt;&lt;span class=&quot;si&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This channel is responsible for broadcasting the progress of the video conversion process, updating a percentage complete element in the UI.&lt;/p&gt;

&lt;p&gt;Next, let’s add the Stimulus controller that will listen for events from this channel and update the UI as it receives them.&lt;/p&gt;

&lt;p&gt;First &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;touch app/javascript/controllers/conversion_progress_controller.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And then:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;// app/javascript/controllers/conversion_progress_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;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;consumer&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;channels/consumer&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;nc&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;progressText&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;nf&quot;&gt;initialize&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;subscription&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;consumer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subscriptions&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;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;channel&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;VideoConversionChannel&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;na&quot;&gt;id&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;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&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;connected&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;_connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;na&quot;&gt;disconnected&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;_disconnected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;na&quot;&gt;received&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;_received&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;nf&quot;&gt;_connected&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;_disconnected&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;_received&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;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;nf&quot;&gt;updateProgress&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;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;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;nx&quot;&gt;updateProgress&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;progress&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;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;progressPercent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&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;progress&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;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;nx&quot;&gt;progressPercent&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;100%&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;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;progressPercent&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;progress&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;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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;progressTextTarget&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;nx&quot;&gt;progressPercent&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This controller contains a lot of Action Cable boilerplate, don’t let it overwhelm you.&lt;/p&gt;

&lt;p&gt;The important parts are the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;channel&lt;/code&gt; we subscribe to in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initialize&lt;/code&gt; method and the  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_received&lt;/code&gt; method. When a new message is broadcast on the channel that the user is subscribed to (from the id in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initialize&lt;/code&gt; method), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_received&lt;/code&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updateProgress&lt;/code&gt;which updates the DOM with the progress value broadcast by Action Cable.&lt;/p&gt;

&lt;p&gt;Let’s wire this controller up to our HTML and start to bring it all together.&lt;/p&gt;

&lt;p&gt;We want to subscribe to updates on a particular user, which means we need to update our show view to connect to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConversionProgress&lt;/code&gt; Stimulus controller.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- views/users/show.html.erb --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- snip --&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-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;%= @user.id %&amp;gt;&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;conversion-progress&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;max-width: 500px; max-height: 500px;&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;user.convert_video&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;err&quot;&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;div&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;We are converting your video. The video is currently &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-conversion-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressText&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;0%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt; processed&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;err&quot;&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;video&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;controls&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;max-width: 100%; max-height: 100%;&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;source&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;%= url_for(@user.profile_video) %&amp;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;/video&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;err&quot;&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;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- snip --&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’ve added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-id&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller&lt;/code&gt; to the video’s parent video. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-id&lt;/code&gt; is used by the Stimulus controller to know which channel to subscribe to updates from, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller&lt;/code&gt; is used to connect the Stimulus controller to the DOM.&lt;/p&gt;

&lt;p&gt;The other change here is adding logic to display the video as-is when the video has been converted, otherwise, we render text that our Stimulus controller will update as it receives updates from Action Cable.&lt;/p&gt;

&lt;p&gt;After making these changes, if you upload a new non-mp4 video to a user and visit the show page, you’ll see the “We are converting your video” text but the percentage processed will never update. That is because we aren’t yet broadcasting the conversion progress to the Action Cable channel.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/04-28-not-progressing-yet.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of plain text displaying 0% progress converting the video&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Fortunately, the FFmpeg gem makes broadcasting progress really simple. Let’s update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transcode&lt;/code&gt; call in our video converter service to broadcast the change:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;n&quot;&gt;movie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transcode&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;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;video_codec: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;libx264&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;audio_codec: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;aac&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;progress&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionCable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;broadcast&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;video_conversion_&lt;/span&gt;&lt;span class=&quot;si&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;id&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;progress&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’ll see our progress text count up from 0% to 100% when we upload a new video that needs conversion. Depending on the video’s properties and your computer’s power, this might be a very fast process or it could take several minutes. Either way, you can now watch the progress in real time!&lt;/p&gt;

&lt;p&gt;When the percentage gets to 100% you’ll notice one final issue blocker that we need to solve. Instead of replacing the video placeholder with the actual video when it reaches 100% conversion, the counter just stays there and the user has to refresh the page to see the video. We can fix that with a little more Action Cable, and a little more Stimulus.&lt;/p&gt;

&lt;p&gt;First, add another Action Cable channel with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g channel ConvertedVideo&lt;/code&gt; and update the subscribed method in the generated _channel.rb file:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ConvertedVideoChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationCable&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;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;subscribed&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;stream_from&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;converted_video_&lt;/span&gt;&lt;span class=&quot;si&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then add a new Stimulus controller for subscribing to the channel and managing updates with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;touch app/javascript/controllers/converted_video_controller.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And add the code to subscribe and handle broadcasts on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvertedVideo&lt;/code&gt; channel.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;// javascript/controllers/converted_video_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;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;consumer&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;channels/consumer&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;nc&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;videoContainer&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;nf&quot;&gt;initialize&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;subscription&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;consumer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;subscriptions&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;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;channel&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;ConvertedVideoChannel&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;na&quot;&gt;id&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;dataset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&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;connected&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;_connected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;na&quot;&gt;disconnected&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;_disconnected&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;na&quot;&gt;received&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;_received&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&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;p&quot;&gt;);&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;nf&quot;&gt;_connected&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;_disconnected&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;_received&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;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;videoElement&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;videoContainerTarget&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;videoElement&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;nx&quot;&gt;data&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This Stimulus controller is very similar to the last controller. In it, we subscribe to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvertedVideoChannel&lt;/code&gt; with the user id. When data is broadcast on the channel, the Stimulus controller looks for a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;videoContainer&lt;/code&gt; DOM element and replaces the content of that element with the data sent from Action Cable. We’ll see what that data looks like next.&lt;/p&gt;

&lt;p&gt;Our goal here is to replace the container of the video placeholder element with the actual video once it has been processed. We can do this with Action Cable by taking advantage of the fact that we can render a view partial to a string and broadcast that string from Action Cable, making it easy to replace DOM content with HTML broadcast in a message.&lt;/p&gt;

&lt;p&gt;To start, let’s add a partial that renders the video element, and add that to our the view. While we’re in the show view, we’ll also connect the new Stimulus controller to the DOM.&lt;/p&gt;

&lt;p&gt;First run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;touch app/views/users/_profile_video.html.erb&lt;/code&gt; in your terminal and then:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- views/users/_profile_video.html.erb --&amp;gt;&lt;/span&gt;

    &lt;span class=&quot;nt&quot;&gt;&amp;lt;video&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;controls&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;max-width: 100%; max-height: 100%;&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;source&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;%= url_for(user.profile_video) %&amp;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;/video&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;c&quot;&gt;&amp;lt;!-- views/users/show.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;data-id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&amp;lt;%= @user.id %&amp;gt;&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;conversion-progress converted-video&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-converted-video-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;videoContainer&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;style=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;max-width: 500px; max-height: 500px;&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;user.convert_video&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;err&quot;&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;div&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;lt;p&amp;gt;&lt;/span&gt;We are converting your video. The video is currently &lt;span class=&quot;nt&quot;&gt;&amp;lt;span&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-conversion-progress-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;progressText&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;0%&lt;span class=&quot;nt&quot;&gt;&amp;lt;/span&amp;gt;&lt;/span&gt; processed&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&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;err&quot;&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%=&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;profile_video&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&quot;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;user:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;err&quot;&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;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;err&quot;&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;/div&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’ve moved the video element to a partial, and updated our show view to render the partial when the video does not need to be converted.&lt;/p&gt;

&lt;p&gt;Take note of the first line above. We’ve added the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;converted-video&lt;/code&gt; Stimulus controller to the video container’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller&lt;/code&gt; attribute. This connects the controller to the DOM and ensures that visitors to the show page are subscribed to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvertedVideo&lt;/code&gt; channel. We’ve also added a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-converted-video-target&lt;/code&gt; attribute to the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. This target is used by the Stimulus controller to replace the progress text with the video element.&lt;/p&gt;

&lt;p&gt;The last step is to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VideoConverter&lt;/code&gt; service to broadcast a message containing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;profile_video&lt;/code&gt; partial on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConvertedVideo&lt;/code&gt; channel after the video has been converted.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    # app/services/video_converter.rb

    def convert!
      return unless @user.convert_video?

      process_video
      update_needs_conversion
      render_processed_video
    end

  private

    # Snip
    def render_processed_video
      partial = ApplicationController.render(partial: &quot;users/profile_video&quot;, locals: { user: @user })
      ActionCable.server.broadcast(&quot;converted_video_#{@user.id}&quot;, partial)
    end
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Here we’re calling a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;render_processed_video&lt;/code&gt; method from our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;convert!&lt;/code&gt; method. This method renders a partial to a string and then broadcasts that string as data, to be picked up and used by our Stimulus controller. Magic.&lt;/p&gt;

&lt;p&gt;Let’s see it in action.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/04-28-final-converter.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user clicking a submit button, seeing a progress bar, and then progress text that counts up as a video converts. When finished, the text is replaced with the video element&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Thanks for making it through this guide! You can find the full source code for this guide on &lt;a href=&quot;https://github.com/DavidColby/rails-video-converter-example&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To recap, today we started with a fresh Rails 6 application. With the power of Stimulus, Active Storage, Action Cable and, most importantly, FFmpeg, we built an app that can convert a user-uploaded video file to mp4 when needed. While the file is converting, we communicate the progress to the user and display the converted user to them without asking them to reload the page.&lt;/p&gt;

&lt;p&gt;To make the code in this tutorial production-ready, besides cleaning up the code and styling things, you should spend time validating file uploads from the user, add a real background job processor, and add error-handling and resilience to the video conversion service.&lt;/p&gt;

&lt;p&gt;You might also add validation for file size to the videos, on both the client and server, and consider more efficient methods for converting videos to mp4 with more powerful FFmpeg functionality.&lt;/p&gt;

&lt;h3 id=&quot;further-resources-and-contact-info&quot;&gt;Further resources and contact info&lt;/h3&gt;

&lt;p&gt;If you’d like to see a production version of this project, take a look at the demo for &lt;a href=&quot;https://vestimonials.tv&quot;&gt;Vestimonials&lt;/a&gt;, the product I’m building when I’m not writing technical articles like this one.&lt;/p&gt;

&lt;p&gt;If you have questions or feedback on this article, you can find me on &lt;a href=&quot;https://twitter.com/davidcolbyatx&quot;&gt;Twitter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Quit Doing Stupid Shit</title>
   <link href="https://colby.so/posts/quit-doing-stupid-shit"/>
   <updated>2021-01-27T00:00:00+00:00</updated>
   <id>https://colby.so/posts/quit-doing-stupid-shit</id>
   <content type="html">&lt;p&gt;At one point in my time at CareerPlug, the company adopted a phrase that I said in passing a few times - “Quit doing stupid shit” - as a company-wide mantra.&lt;/p&gt;

&lt;p&gt;Was that a great phrase for the company to rally around? Probably not.&lt;/p&gt;

&lt;p&gt;Most office workers do some amount of pointless busy work in their day (&lt;a href=&quot;https://www.simonandschuster.com/books/Bullshit-Jobs/David-Graeber/9781501143335&quot;&gt;some more than others, perhaps&lt;/a&gt;), but saying that out loud can rub people the wrong way. No one wants to hear that their bosses think they’re doing stupid shit!&lt;/p&gt;

&lt;p&gt;In spite of the insulting phrasing, there’s a lesson to learn from the mantra.
&lt;!--more--&gt;&lt;/p&gt;

&lt;p&gt;After leaving CareerPlug, I’ve been reflecting on the events that led me to the end of my time there. Chief amongst those reasons was intense burnout. After ten years of grinding to build the business, my brain and body needed a break, and they told me that by breaking down.&lt;/p&gt;

&lt;p&gt;By the end of 2020, I was constantly sick, my cholesterol was through the roof, I was exhausted, and incapable of focusing on anything for more than a few minutes. I got diagnosed with two separate autoimmune disease in Q4. Both diseases are genetic and weren’t caused by the burnout; however, I believe the timing of the emergence of symptoms is directly related to the burnout I experienced.&lt;/p&gt;

&lt;p&gt;What caused the burnout? Doing stupid shit, repeatedly.&lt;/p&gt;

&lt;p&gt;I didn’t follow my own advice, even as I pushed for my team and the company to focus on what mattered most. I did all kinds of things that didn’t need to be done and put pressure on myself to do more and more, even when I knew that I was doing things that were unimportant.&lt;/p&gt;

&lt;h2 id=&quot;why-didnt-i-follow-my-own-advice&quot;&gt;Why didn’t I follow my own advice?&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;Focusing is hard. Especially in a bootstrapped business when resources are extremely tight. There’s always something else to do, and only so many people to delegate to. I took on tasks to avoid burning my team out, and paid the price.&lt;/li&gt;
  &lt;li&gt;Pushing back doesn’t come naturally to me. I like to help people. Other people realized that, and got into the habit of coming to me to solve things for them. I didn’t do a good enough job protecting my time, especially in the early years, leading to me inheriting many low value tasks that couldn’t be delegated.&lt;/li&gt;
  &lt;li&gt;I was too optimistic. I committed to things that were not realistic because I believed we would find a way to get them done. I set outrageous deadlines, missed them, and then tried to push harder to make up for the missed deadlines.&lt;/li&gt;
  &lt;li&gt;I was unhappy. The work the I love doing is building. CareerPlug was no longer primarily interested in building. The company got bigger, teams began competing for resources, new product work became harder and more fraught. Every change was mentally taxing. The joy in the work went away and I tried to grind harder in response, despite the bad feelings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-did-i-learn-from-burnout-land&quot;&gt;What did I learn from burnout land?&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;Focus is a company-wide effort. If you preach focus at an individual level, but everyone has a different focus, no one will focus on anything.&lt;/li&gt;
  &lt;li&gt;Work towards clear, measurable outcomes. If you don’t, you will end up doing all kinds of work that has no impact on the business without having any idea what work matters and what work is worthless. If you don’t know where you’re trying to go, you won’t get there.&lt;/li&gt;
  &lt;li&gt;Being a short-term pessimist has advantages. Optimism is necessary to succeed in the long term - big wins come from big bets. In the short term, when it comes to delivery, being a pessimist gives you space to deal with unseen problems without blowing a deadline. Stay positive about the future, be pessimistic about what you will deliver in the short term.&lt;/li&gt;
  &lt;li&gt;Loving your work can carry you through hard times, but you’re on thin ice. I worked long hours for years without feeling the pain, largely because the work was energizing. Once the work was no longer energizing, I was in an unhealthy place with a rapidly depleting gas tank.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving on to the next phase in my career, I’m hopeful I will be able to identify burnout-producing busy work more effectively, for myself and, more importantly, for my company. Tune out the noise, focus on what matters, and measure the outcomes your work produces. Easy.&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Building a custom Stimulus generator for Rails</title>
   <link href="https://colby.so/posts/building-a-custom-stimulus-generator-for-rails"/>
   <updated>2020-11-30T00:00:00+00:00</updated>
   <id>https://colby.so/posts/building-a-custom-stimulus-generator-for-rails</id>
   <content type="html">&lt;p&gt;If you’ve spent any time working with Ruby on Rails, you know that using generators can save you from a lot of console commands and copy/pasting from one file to the next. Need a new model? Just run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g model MyNewModel name:string description:string&lt;/code&gt; and you’re set.&lt;/p&gt;

&lt;p&gt;Rails covers most of the common use cases for you with built-in generators. If the built-in generators don’t meet your needs, you can get fancy by creating your own custom generators. Custom generators can save you time any time you’re finding yourself running lots of touch commands and copy/pasting boilerplate into new files.&lt;/p&gt;

&lt;p&gt;Although Rails provides built-in generators for most things that Rails projects need, one common use case that’s missing is a generator for new Stimulus controllers.  &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt; , a modest JavaScript framework created by the team at Basecamp, is an increasingly common part of the Rails stack.
&lt;!--more--&gt;
Despite its growing popularity, Rails does not provide a generator out of the box (as of Rails 6.0). Without a generator, every new Stimulus controller requires a manual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;touch app/javascript/controllers/my_new_controller.js&lt;/code&gt; in the console and then copying from another controller, changing the class name, and otherwise doing stuff uninteresting stuff before you start building something new. No fun.&lt;/p&gt;

&lt;p&gt;Today we’re going to solve this problem by learning how to build our own custom generator for Rails. Our custom generator will create new a new Stimulus controller in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers&lt;/code&gt; directory when we run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g stimulus ControllerName&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let’s get started.&lt;/p&gt;

&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;

&lt;p&gt;I’m writing this assuming that you have a Rails 6 project and you’re working from that project in your terminal.&lt;/p&gt;

&lt;p&gt;If you don’t have a Rails project handy, just run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails new stimulus_generator_project&lt;/code&gt; and then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd stimulus_generator_project&lt;/code&gt; to follow along.&lt;/p&gt;

&lt;h3 id=&quot;generating-a-generator&quot;&gt;Generating a generator&lt;/h3&gt;

&lt;p&gt;Rails is known for valuing developer experience, and Rails generators are a prime example of that love for developers. Rails provides a ton of generators out-of-the-box including a generator to generate new generators. Very meta stuff, and very convenient for us.&lt;/p&gt;

&lt;p&gt;We’ll use the generator generator to generate a new Stimulus generator. Still with me? Great. Run this in your console and let’s move on from generating generators:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;rails g generator stimulus
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This applies Rails magic to create the bones of our generator, named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulus&lt;/code&gt;, in the lib directory. You’ll notice that inside of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/generators/stimulus&lt;/code&gt; directory there are a few files and an empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;templates&lt;/code&gt; directory. We’ll briefly talk about what each of these items is and then we’ll create our generator.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-30-generate-output.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot showing the console output of the rails g generate stimulus command run in the previous section.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;stimulus_generator.rb:&lt;/em&gt; This is the file that runs when you type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g stimulus&lt;/code&gt; in your console. The class defined here will manage creating a new file each time the generator runs, and populating that file with the content from our template Stimulus controller.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;USAGE:&lt;/em&gt; This file contains instructions for users of your generator. The contents of this file are printed when you type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g stimulus&lt;/code&gt; in your console.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Templates:&lt;/em&gt; The templates directory contains any files that your generator will copy when it runs. In our case, we’ll just have one file. These template files can access and insert variables into the template, when needed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;stimulus_generator_test.rb:&lt;/em&gt; The generator generator helpfully creates a test file, which can be run (in our case) with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rake test&lt;/code&gt;. Our generator is simple, but we’ll add a test to make sure it works as we expect.&lt;/p&gt;

&lt;h3 id=&quot;adding-our-code&quot;&gt;Adding our code&lt;/h3&gt;

&lt;p&gt;As a reminder, we’re building a generator to create a new Stimulus controller. This generator will only accept a name as an argument and will always create the controller in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers&lt;/code&gt; directory when it runs.&lt;/p&gt;

&lt;p&gt;Let’s first add a test to our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stimulus_generator_test&lt;/code&gt; file. We’ll use this test to make sure our generator works without having to generator a Stimulus controller we don’t want.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/generators/stimulus/stimulus_generator_test.rb&lt;/code&gt; add a test that runs our generator and validates that a new file is generated as expected:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;test_helper&apos;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;generators/stimulus/stimulus_generator&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StimulusGeneratorTest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Generators&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;TestCase&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;tests&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;StimulusGenerator&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;destination&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;root&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;s1&quot;&gt;&apos;tmp/generators&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;setup&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:prepare_destination&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;It generates a controller in the app/javascript/controllers directory&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;run_generator&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Hello&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;assert_file&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;app/javascript/controllers/hello_controller.js&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;When we run rake test the test will fail, which is expected since our generator hasn’t been built yet. Let’s work on making it pass.&lt;/p&gt;

&lt;p&gt;First, create the template file that our generator will copy each time it runs. This file will live in the templates directory:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;touch lib/generators/stimulus/templates/controller.js
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;In this new file, we’ll insert the content of our Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;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;nc&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;nf&quot;&gt;initialize&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;// Called once, when the controller is first instantiated&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nf&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;// Called any time the controller is connected to the DOM&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nf&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;// Called any time the controller is disconnected from the DOM&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;My template includes the basic structure of a Stimulus controller plus the lifecycle methods I find myself using most often, that’s what makes the most sense to me. You can include more lifecycle methods or just leave out everything but the class definition, you’re the boss of your own template file.&lt;/p&gt;

&lt;p&gt;With our template file created, we can go into our generator file and tie the generator and template together:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StimulusGenerator&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Generators&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;NamedBase&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;source_root&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;expand_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;templates&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__dir__&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;create_controller&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;s1&quot;&gt;&apos;controller.js&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&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;app/javascript/controllers/&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;_controller.js&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Each time rails g stimulus Something is called, any method in this file will run. In our case, we just create a new file from our template using the template method (&lt;a href=&quot;https://gist.github.com/DavidColby/49a99e51c8f31217254438e30012dd56&quot;&gt;provided by Thor&lt;/a&gt;, if you’re interested in the inner workings of Rails generators). If we want to handle conflicting file names, make our file name generation smarter, process additional arguments or otherwise make our generator more powerful we can add additional methods to do so. For our purposes, we will keep things simple.&lt;/p&gt;

&lt;p&gt;With our template and our generator class built, let’s see if our tests pass:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-30-test-output.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of the result of running rake test in the console with all tests passing.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Perfect! We’ve got a working generator and we can run that generator with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g stimulus CoolIdea&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve followed along with me through this tutorial, when we run the generator our new controller will be created at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascript/controllers/cool_idea_controller.js&lt;/code&gt; and it will be populated with the content from our controller template file.&lt;/p&gt;

&lt;p&gt;Congratulations on making it this far! You’re a star.&lt;/p&gt;

&lt;p&gt;Our last step is to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;USAGE&lt;/code&gt; file for our generator with useful information for others who encounter our generator in the future. Something like this will work fine:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-markdown&quot; data-lang=&quot;markdown&quot;&gt;Description:
Stubs out a new Stimulus controller.
This generator accepts one argument, the name of the controller, CamelCased or under_scored.
Example:
&lt;span class=&quot;sb&quot;&gt;`rails generate stimulus Thing`&lt;/span&gt;
This will create a new Stimulus controller at &lt;span class=&quot;sb&quot;&gt;`app/javascript/controllers/thing_controller.js`&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now when we run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails g stimulus&lt;/code&gt; our help text will be printed to the terminal. The generator is pretty simple today, but in the future we might add additional arguments or options. Starting with documentation from day one is always the better plan.&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;

&lt;p&gt;Today we learned how we can use the &lt;strong&gt;magic&lt;/strong&gt; of Rails to create our own generators and we used that knowledge to fill a gap in the Rails defaults by creating a simple generator to add new Stimulus controllers.&lt;/p&gt;

&lt;p&gt;While our example was simple, the same technique can be applied to create sophisticated new generators for Service Objects, complex Stimulus controllers, or any other resource you find yourself needing. In addition to creating our own custom generators, we can also use this knowledge to change the default generators provided by Rails. Overriding the default model or controller generators can help your organization enforce code standards across an organization or just save you from copying boilerplate across your project.&lt;/p&gt;

&lt;p&gt;For more reading on the power of Rails generators, start with the &lt;a href=&quot;https://guides.rubyonrails.org/generators.html&quot;&gt;Rails guide&lt;/a&gt; on generators.
Finally, if creating your own Stimulus generator isn’t a chore you’re looking forward to, I’ve &lt;a href=&quot;https://github.com/DavidColby/rails-stimulus-generator&quot;&gt;published a gem&lt;/a&gt; you can use to add Stimulus generator just like the one in this tutorial to your Rails projects.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Shape Up reflections: Volume 2</title>
   <link href="https://colby.so/posts/shape-up-reflections-volume-2"/>
   <updated>2020-11-28T00:00:00+00:00</updated>
   <id>https://colby.so/posts/shape-up-reflections-volume-2</id>
   <content type="html">&lt;p&gt;&lt;em&gt;This post is a lightly edited version of an internal update I sent to everyone at CareerPlug after the completing our second product cycle, modeled on Basecamp’s &lt;a href=&quot;https://basecamp.com/shapeup&quot;&gt;Shape Up&lt;/a&gt; method. I am making these updates public in hopes that others will find value in the lessons learned from a real team adopting a fairly radical new product development process.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href=&quot;https://www.colby.so/posts/shape-up-reflections-volume-1/&quot; title=&quot;Shape Up Reflections Volume 1&quot;&gt;first update&lt;/a&gt; includes more information about our product team and our company, for context. Italicized text are extra context I’ve added for the public version of this update.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Let’s dig in.  -David&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;

&lt;!--more--&gt;
&lt;p&gt;Team - Cycle 2 is in the books! This was our second cycle following our new product development process and our first with 6 weeks of cycle time instead of 4. We made great progress on some key initiatives this cycle and the team learned a lot of lessons to make future cycles more effective. We’ll keep learning as we go, but we should feel proud of the learning we’ve done in the first two cycles and the work we’ve produced.&lt;/p&gt;

&lt;p&gt;Below you’ll find an overview of the things that worked well, the things that didn’t work, and our lessons learned for future cycles. At the end of this reflection, I’ll look at some of the work we released in the first cycle and review the results so far.*&lt;/p&gt;

&lt;h3 id=&quot;what-worked&quot;&gt;What worked&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Six weeks is a better cycle length&lt;/strong&gt;
Extending cycle length from four to six weeks gave us more time to build impactful features and more space to explore options and pivot when needed. The biggest changes in this cycle would not have been possible to build in four weeks.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Small changes and open design space can go a long way&lt;/em&gt;
In Cycle 2 we released a number of small changes that didn’t add more mental overhead for our users but still made their experience with the product better. This is possible because of the work we’ve done to make the product more focused. Over time, the product will get less focused and become harder to reason about without concerted effort to keep things as simple and as relevant to the user’s goals as possible.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We’re adapting quickly and learning from mistakes&lt;/em&gt;
This is still a very new process for the product org and for the company. We don’t expect it all to just work right away. As we encounter new issues or something unexpected goes wrong, we’re identifying the problem and implementing solutions so we’re better prepared for next time. This will continue being a theme for the next few cycles - getting a little better each time is the goal.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;An example of an in-cycle adjustment is that each project team now updates the status of their project with a red/yellow/green indicator so that we can identify off track projects and quickly review the status of all projects as needed. &lt;em&gt;[David note: This small change was implemented as a new field in Jira, which engineering and QA use to track their in-cycle work. Hill charts would be more useful, but we haven’t adopted Basecamp as an organization.]&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-didnt-work&quot;&gt;What didn’t work&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Poorly written pitches&lt;/strong&gt;
A few pitches did not set the project team up for success. These were written by me and the miss on these was down to me not doing a good enough job in the shaping process.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The pitch for new account onboarding included way too many ideas, far more than could be achieved in a single six-week cycle. That pitch should have been scoped down to focus on a single impactful, achievable outcome and the rest of the work should have been included in a future pitch&lt;/li&gt;
  &lt;li&gt;The pitch for the new activation process needed to have the content of the page written and approved before the project began. Leaving marketing copy and pricing page layout copy out of the pitch made it difficult to build the right experience and led to way too much back and forth to agree on details that aren’t in the project team’s purview.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;[David note: One of our major challenges has been finding the right level of detail to include in pitches. I left too many things open to interpretation in this past cycle and teams spent more time spinning their wheels than they should have. Finding the right balance is tough!]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communication needs to be better&lt;/strong&gt;
Communication inside of the product org and to other teams was not as good as it should have been. This isn’t just a Shape Up challenge. Communicating clearly, thoughtfully, and in a timely manner is hard and we’re still learning how best to make it all tie together.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Inside the product org, there were several instances when a project team got off track on a project and did not raise the flag to their manager or the pitch owner quickly enough. This delay led to misses that could have been avoided with faster, more effective communication&lt;/li&gt;
  &lt;li&gt;Outside of the product org, several projects needed significant input or sign off from other teams and we did not communicate effectively with those teams, leading to delays in projects and missed requirements.
[David note: Communication is hard and process is only part of the challenge. Our team is tight knit, kind, and supportive, but we also have some ingrained habits like not asking for help soon enough (because we don’t want to bother our peers who are busy) that will take a concerted effort to overcome. Shape Up isn’t a magic bullet and you’ve still got to do the work!]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Outcome tracking isn’t great&lt;/em&gt;
Tracking outcomes is very manual and outcome definition is unclear.
Since desired outcomes are just being tracked on the pitch, looking back at the work in a cycle and identifying how many projects achieved their desired outcome is difficult and time-consuming. This difficulty is compounded by the vague outcomes on some projects, particularly projects aimed at bringing in functionality to Voyager that exists in Classic but doesn’t have a clear business objective. /[David note: Voyager is the new version of our core software, Classic is the old version. We’ve migrated most of our user base to Voyager but the work to bring the rest of over has been difficult to fit into the Shape Up framework since the business value for some of these changes is quite low.]/&lt;/p&gt;

&lt;h3 id=&quot;what-we-learned&quot;&gt;What we learned&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Communication is (still) key&lt;/strong&gt;
If we write really clear product pitches, hand the pitch to the project team cleanly, and stay tightly connected to the pitch owner while we’re building, the work goes smoothly.&lt;/p&gt;

&lt;p&gt;When we miss on one of those three key communication pieces, things get bumpy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitches need more definition&lt;/strong&gt;
Through the first two cycles I intentionally left pitches open for more interpretation by the product teams. The reason for this is that our team, unlike the Basecamp team who created Shape Up, doesn’t have a huge amount of ingrained UX design experience on the selection committee. Instead of closing off the opportunities for our UX designers to do what they do best, I created pitches that left them room for research; however, I went too far in this direction and some of our pitches haven’t been clear enough to be really actionable and achievable in the appetite we’ve assigned.&lt;/p&gt;

&lt;p&gt;In upcoming cycles we’ll need to find ways to include the UX team in the creation of pitches so that work is more clearly defined before it reaches the betting table or gets assigned to a project team.&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What’s next&lt;/h3&gt;
&lt;p&gt;Cycle 3 is a holiday-shortened cycle. We’ve intentionally committed to less work in this cycle (no more than 4 weeks per project team) and we’re keeping the work more internally focused so that we can comfortably launch these changes when they’re ready even if we’re short staffed on the client-facing teams.&lt;/p&gt;

&lt;h3 id=&quot;cycle-1-early-results&quot;&gt;Cycle 1 early results&lt;/h3&gt;
&lt;p&gt;As mentioned above, outcome tracking is more manual than we would like right now, but we can identify the impact that some of the Cycle 1 launches have had so far:
&lt;em&gt;[David note: I removed the individual results here since they contain a lot of pretty confidential information about our client base. Each update was structured as a link to the project in our product management tool + a brief run down of the current state of the project vs. the expected outcome in the original pitch. In general, the work we released in our first cycle performed reasonably well, with a couple that underperformed expectations and a couple that exceeded expectations. Sharing outcomes internally has been very well received - our product organization’s results are harder to measure than other teams and the transparency into how our work is performing has helped build trust. Happy to share more details on the structure privately, for anyone interested.]&lt;/em&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building an instant search form with Stimulus.js and Rails</title>
   <link href="https://colby.so/posts/live-search-with-stimulus-and-rails-6"/>
   <updated>2020-11-15T00:00:00+00:00</updated>
   <id>https://colby.so/posts/live-search-with-stimulus-and-rails-6</id>
   <content type="html">&lt;p&gt;August 5, 2021 update: Since publishing this post, &lt;a href=&quot;https://turbo.hotwired.dev/handbook/introduction&quot;&gt;Turbo&lt;/a&gt; was released. While the method described here for building a live search interface with Stimulus still works just fine, if you’re using Stimulus, you might be interested in the other half of Hotwire on the web. In August of 2021, I published a post describing how to build the same interface described here with hotwire-rails, instead of just Stimulus.&lt;/p&gt;

&lt;p&gt;Today we’re going to build a search form that instantly filters results using Ruby on Rails and &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt;, a “modest” JavaScript framework designed to add small bits of interactivity without a lot of heavy lifting.&lt;/p&gt;

&lt;p&gt;When we’re finished, we’ll have a list of Players that users can search by name. As they type in the search box, we’ll make a request to the server for players that match and display the results on the frontend (almost) instantly. Here’s what the end result will look like:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-15-demo.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing a search term into a search form and seeing results in a list below the form.&quot; /&gt;
&lt;!--more--&gt;
I’m writing this assuming that you’ve already got Rails installed on your machine and that you are generally familiar with Rails and JavaScript. If you’re just getting started with either, you can still follow along, but you might find some things confusing!&lt;/p&gt;

&lt;p&gt;I’m also not going to do a full overview of how Stimulus works, since the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;official docs&lt;/a&gt; do that well. Instead, our Stimulus work will be focused on building the desired behavior in Stimulus and connecting our Stimulus code to our HTML.&lt;/p&gt;

&lt;p&gt;I’m going to walk through this step-by-step from a new Rails 6 project. If you want to follow along from an existing project that already has Stimulus and rails-ujs installed, skip slightly ahead to the &lt;em&gt;Adding a search form&lt;/em&gt; section.&lt;/p&gt;

&lt;p&gt;Let’s get started.&lt;/p&gt;

&lt;h3 id=&quot;setting-up-our-project&quot;&gt;Setting up our project&lt;/h3&gt;

&lt;p&gt;First, we’ll create our Rails project, specifying that we want to include Stimulus:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;rails new stimulus_live_search_demo --webpack=stimulus
cd stimulus_live_search_demo
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Then, we’ll create our database and scaffold up a Player resource that our users will be soon be searching against:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;rails db:create
rails generate scaffold player name:string
rails db:migrate
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;That’s it — that’s all the setup we need to do. Now we can dive into the fun stuff.&lt;/p&gt;

&lt;h3 id=&quot;adding-a-search-form&quot;&gt;Adding a search form&lt;/h3&gt;

&lt;p&gt;Our users will need a search form if we want them to be able to search. We’ll first create a partial for our search form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;mkdir app/views/shared
touch app/views/shared/_search_form.html.erb
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And then update app/views/players/index.html.erb to render the search form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/players/index.html.erb --&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;notice&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;notice&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;h1&amp;gt;&lt;/span&gt;Players&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&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;shared/search_form&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;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;player&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;player: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;player&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;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;br&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;s1&quot;&gt;&apos;New Player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And add our player partial, which we’ll use later to render our search results:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;touch app/views/players/_player.html.erb
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/players/_player.html.erb --&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;player&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our search form partial looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/shared/_search_form.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;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;player_search_index_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;form&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&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;ss&quot;&gt;autocomplete: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;off&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;You’ll notice that our search form url references a controller that we haven’t created yet, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; controller.&lt;/p&gt;

&lt;p&gt;While we could build a search method in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayersController&lt;/code&gt; creating a separate controller lets us keep our controller actions RESTful and is a little more Rails-y, in my opinion.&lt;/p&gt;

&lt;p&gt;Let’s add our controller now:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;rails generate controller PlayerSearch index
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PlayerSearchController&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;layout&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;false&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;@players&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Player&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;name LIKE ?&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;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;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;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Notice the layout false which ensures that our index view will only render the HTML in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/player_search/index.html.erb&lt;/code&gt;. If we don’t set layout to false, every search request we make will render our index view inside of the layout defined in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/views/layouts/application.html.erb&lt;/code&gt; — we don’t want that since we’re planning to insert the search results into a piece of an existing page.&lt;/p&gt;

&lt;p&gt;Next we’ll add the content of our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PlayerSearch&lt;/code&gt; index view. Our view simply loops over an array of players and renders the player partial each iteration.&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/player_search/index.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@players&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;player&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;players/player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;player: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;player&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we’ll update our routes file so that we can access thePlayerSearch index method.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;:players&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:player_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &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;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now that we have the search form rendering and the controller action built, our next step is to implement the Stimulus search controller.&lt;/p&gt;

&lt;h3 id=&quot;the-stimulus-controller&quot;&gt;The Stimulus controller&lt;/h3&gt;

&lt;p&gt;First, let’s create our Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;touch app/javascript/controllers/search_controller.js
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This controller has two jobs — submitting the search form as the user types and handling the response from the server each time a search query completes.&lt;/p&gt;

&lt;p&gt;Let’s implement form submission first, in a function we’ll call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;search() {
  Rails.fire(this.formTarget, &apos;submit&apos;)
}
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Rails.fire is, a poorly documented function available through &lt;a href=&quot;https://github.com/rails/rails/blob/master/actionview/app/assets/javascripts/rails-ujs/utils/event.coffee&quot;&gt;rails-ujs&lt;/a&gt;. It takes an element (formTarget, in this case, which we’ll attach add to our HTML shortly) and the name of an action to trigger (like click or submit) and then triggers that event on the element. Despite the poor documentation around this method, it is the &lt;a href=&quot;https://www.betterstimulus.com/rails-ujs/submit-form.html&quot;&gt;preferred method&lt;/a&gt; of submitting forms programmatically in Stimulus controllers.&lt;/p&gt;

&lt;p&gt;To access Rails.fire in our Stimulus controller, we need to assign Rails to the window. We can do that with a small update to app/javascript/packs/application.js.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;Rails&lt;/span&gt; &lt;span class=&quot;o&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@rails/ujs&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;nf&quot;&gt;require&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;turbolinks&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;nf&quot;&gt;start&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@rails/activestorage&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;nf&quot;&gt;start&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;channels&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;controllers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we can submit the search form programmatically.&lt;/p&gt;

&lt;p&gt;Next, let’s add the function to handle search results from the server. We’ll do this with a new function in our Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;handleResults&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;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;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;xhr&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;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;detail&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;resultsTarget&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;nx&quot;&gt;xhr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This function grabs the inbound response from the server and replaces the contents of a (yet-to-be-defined) DOM element with the portion of the response we care about.&lt;/p&gt;

&lt;p&gt;If the variable assignment looks mysterious to you, check out the Rails guide &lt;a href=&quot;https://guides.rubyonrails.org/working_with_javascript_in_rails.html#rails-ujs-event-handlers&quot;&gt;Working with JavaScript in Rails&lt;/a&gt; which describes the event handlers available in rails-ujs and the parameters that those event handlers include. In our case, we’ll be using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ajax:success&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;When finished, our complete Stimulus controller looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;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;nc&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;results&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;form&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;nf&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;console&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Connected!&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;nf&quot;&gt;search&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;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fire&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;formTarget&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;submit&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;nf&quot;&gt;handleResults&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;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;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;xhr&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;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;detail&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;resultsTarget&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;nx&quot;&gt;xhr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;h3 id=&quot;connecting-the-controller-to-the-frontend&quot;&gt;Connecting the controller to the frontend&lt;/h3&gt;

&lt;p&gt;The Stimulus controller is ready — our last step is to connect our Stimulus controller and our HTML. To do so, we’ll need to update the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index&lt;/code&gt; view in our Players view and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search_form&lt;/code&gt; partial we created earlier.&lt;/p&gt;

&lt;p&gt;First, the updates to the the index view:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/players/index.html.erb --&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;notice&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;notice&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;h1&amp;gt;&lt;/span&gt;Players&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;live-search&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;shared/search_form&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;live-search.results&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;@players&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;player&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;player&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;player: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;player&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;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;br&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;s1&quot;&gt;&apos;New Player&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;new_player_path&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;The data-controller attribute added to our parent div connects our Stimulus controller to the DOM. The data-target attribute on the results container div is used by handleResults in our Stimulus controller to render the results of our search when a request is submitted. If any of this syntax is confusing, the best place to start is the &lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;Stimulus handbook&lt;/a&gt; which explains how Stimulus uses these data attributes in detail.&lt;/p&gt;

&lt;p&gt;Last step before we can start typing search terms — updating our search form partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/shared/_search_form.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;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;player_search_index_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;method: :get&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;ajax:success-&amp;gt;live-search#handleResults&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;live-search.form&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&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;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;input-&amp;gt;live-search#search&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;autocomplete: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;off&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;We’re doing a few things here, let’s walk through it.&lt;/p&gt;

&lt;p&gt;First, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; on our form element calls our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;handleResults&lt;/code&gt; method each time this form is submitted successfully. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-target&lt;/code&gt; on the form element gives us an easy reference to the form in the Stimulus controller which we use in the search function to submit the form.&lt;/p&gt;

&lt;p&gt;On the text field, we add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; that calls the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;search&lt;/code&gt; method each time the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event&quot;&gt;input event&lt;/a&gt; is fired on this input. Because using the browser’s autocomplete functionality on a search form isn’t usually desired, we also turn off autocomplete. You can skip turning off autocomplete if you like, it won’t bother me.&lt;/p&gt;

&lt;p&gt;That’s it — as users type in the search field we automatically send a request to the server to find matching players and update the players list so users can see results in real time. Add a few players to your database and try it out!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-15-second-demo.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a user typing a search term into a search form and seeing results in a list below the form.&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;improving-our-search-method&quot;&gt;Improving our search method&lt;/h3&gt;

&lt;p&gt;As you followed along with this tutorial, you might have noticed that our implementation submits the form every time the input event fires. This means that if the user types 10 characters in a row we’ll make 10 separate search requests to the server. In a toy application like ours that’s okay, but we can do a little better.&lt;/p&gt;

&lt;p&gt;Instead of submitting the form each time an input event fires, we can instead wait for the user to stop typing before submitting the form. One way of doing this is by using a debounce function to delay form submission until after the user has stopped an action for a period of time. A simple implementation of debouncing that makes use of &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout&quot;&gt;setTimeout&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout&quot;&gt;clearTimeout&lt;/a&gt; looks like this:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;search&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;clearTimeout&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;timeout&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;timeout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTimeout&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;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;fire&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;formTarget&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;submit&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;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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;This change to delay the Rails.fire call until 200ms after the last input event fires.&lt;/p&gt;

&lt;p&gt;In a real world application, instead of implementing your own debounce function, you should consider a resilient, well-architected solution from a library like &lt;a href=&quot;https://lodash.com/&quot;&gt;Lodash&lt;/a&gt;. You can read more about debouncing &lt;a href=&quot;https://css-tricks.com/debouncing-throttling-explained-examples/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;

&lt;p&gt;Together we’ve built an instant search results with a very small amount of Stimulus and a few data attributes in our HTML. What’s really cool about this implementation is that it is reusable anywhere in your application that you want the same behavior. Since the Stimulus controller just submits a form and replaces data on the page, you could easily reuse the exact same Stimulus controller to build a search UX for any resource in your application!&lt;/p&gt;

&lt;p&gt;From the base we’ve built in this demo application, we could add more complex search and filtering options and implement loading states to improve user experience on slower requests.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;h3 id=&quot;helpful-resources&quot;&gt;Helpful resources&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;The full code for this repository can be found on  &lt;a href=&quot;https://github.com/DavidColby/stimulus_search_demo&quot;&gt;Github&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://stimulus.hotwired.dev/handbook/introduction&quot;&gt;The Stimulus handbook&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://guides.rubyonrails.org/working_with_javascript_in_rails.html&quot;&gt;Working with JavaScript in Rails&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/rails/rails/tree/master/actionview/app/assets/javascripts/rails-ujs&quot;&gt;The rails-ujs source code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</content>
 </entry>
 
 <entry>
   <title>Building a simple commenting form with Stimulus and Rails 6</title>
   <link href="https://colby.so/posts/building-a-simple-commenting-form-with-stimulus-js-and-rails-6"/>
   <updated>2020-11-07T00:00:00+00:00</updated>
   <id>https://colby.so/posts/building-a-simple-commenting-form-with-stimulus-js-and-rails-6</id>
   <content type="html">&lt;p&gt;If you use Ruby on Rails, you’ve probably heard of &lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;Stimulus&lt;/a&gt;, a “modest” JavaScript framework for sprinkling in small bits of JavaScript to your mostly-HTML front-end. Instead of reaching for the monster JavaScript frameworks that have become very popular in recent years, Stimulus gives you the tools to add just enough interactivity while relying on plain-old Rails and HTML as often as possible.&lt;/p&gt;

&lt;p&gt;To show you the power and simplicity of Stimulus we’re going to build a simple Stimulus-powered implementation of a commenting form. When we’re finished we’ll have a comment form that skips a page turn and neatly renders our new comments as we create them, without jQuery or JS partials — just a little bit of Stimulus-flavored JavaScript and HTML.
&lt;!--more--&gt;
I’m going to walk through this step-by-step from a new Rails 6 project. If you want to follow along from an existing project that already has Stimulus and rails-ujs installed, skip ahead to the &lt;strong&gt;Creating our comment form&lt;/strong&gt; section.&lt;/p&gt;

&lt;p&gt;I’m writing this assuming that you’ve already got Rails installed on your machine and that you are generally familiar with Rails and Javascript. If you’re just getting started with either, you can still follow along, but you might find some things confusing!&lt;/p&gt;

&lt;p&gt;I’m also not going to do a full overview of how Stimulus works, since the &lt;a href=&quot;https://stimulus.hotwired.dev//handbook/introduction&quot;&gt;official docs&lt;/a&gt; do that well. Instead, our Stimulus work will be focused on integrating Stimulus with Rails.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before we start, a note: Since this tutorial was written, Stimulus 2 has been released. The below code will all work just fine with Stimulus 2; however, you’ll get some deprecation warnings in your web console. The &lt;a href=&quot;https://github.com/DavidColby/stimulus_comments_demo&quot;&gt;companion repository&lt;/a&gt; for this tutorial has been updated for Stimulus 2 if you’d like to see what has changed in Stimulus 2.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-setup&quot;&gt;The Setup&lt;/h3&gt;
&lt;p&gt;First, let’s create our Rails project and setup our database, from our terminal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;rails new stimulus_comments_demo --webpack=stimulus
cd stimulus_comments_demo
rails db:create
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--webpack=stimulus&lt;/code&gt; here is the important part. This will ensure that our new project is set up correctly and save us a lot of copy/pasting boilerplate later.&lt;/p&gt;

&lt;p&gt;Our demo application will have Projects and each Project will have any number of comments.&lt;/p&gt;

&lt;p&gt;Let’s setup the basics of that structure next:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;rails generate scaffold project name:string
rails generate model comment body:text project:references
rails db:migrate
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now that our project has resources, let’s update our routes.rb file to set a root path:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;:projects&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;root&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;projects#index&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, our Projects have many comments — let’s add that association to our project model&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:comments&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Whew — the Rails setup work is out of the way and we now have a project ready for our new stimulus comment form.&lt;/p&gt;

&lt;p&gt;Verify everything is working by booting up your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails server&lt;/code&gt; and visiting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:3000&lt;/code&gt; . You should see our unstyled projects page. Go ahead and add a new Project while you’re there since we’ll need that later.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-07-commenting-form-unstyled.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of the comment form that we just created&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;creating-our-comment-form&quot;&gt;Creating our comment form&lt;/h3&gt;
&lt;p&gt;With our Projects project setup, we can start building out comments. Let’s start by setting up our Rails controller for comments and adding views to support comments, from our terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;rails g controller Comments
touch app/views/comments/_show.html.erb
touch app/views/comments/_form.html.erb
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our Rails controller for comments will handle creation of new comments and, if you’ve ever handled a form POST in Rails, it will look pretty familiar.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set_project&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;n&quot;&gt;comment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Comment&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;comment_params&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;project&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&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;save&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Instead of redirecting like we normally would, we render a partial for our Stimulus controller to handle
&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Note that we&apos;re just rendering HTML here, no need to render jbuilders or js partials
&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;s1&quot;&gt;&apos;show&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;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;ss&quot;&gt;status: :ok&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;partial: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;form&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;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;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;comment_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;:comment&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;:body&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;set_project&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@project&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Project&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;:project_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;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Before we move on to our views, let’s update our routes file so that our comments controller is accessible.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&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;:projects&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;:comments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:create&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;root&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;to: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;projects#index&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next, we’ll add content to our comment view files, and update our project show view to render comments for the project.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/projects/show.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;comments&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comments&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@project&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;h2&amp;gt;&lt;/span&gt;Comments&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&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-target=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comments.commentList&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;@project&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;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;comment&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;comments/show&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;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;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&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;comments/form&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;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/comments/_show.html.erb --&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;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;/div&amp;gt;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/comments/_form.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;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;model: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Comment&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;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project_comments_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@project&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;ajax:success-&amp;gt;comments#createSuccess&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;comments.commentBody&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;placeholder: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&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;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Save&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;There are some important concepts in these views. Let’s walk through each one.&lt;/p&gt;

&lt;p&gt;Our project show view connects to our Stimulus comments controller on line 2 with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-controller=&quot;comments&quot; &lt;/code&gt;. The controller HTML element needs to wrap both our comment list and our comment form, otherwise the controller actions and targets on those elements won’t function.&lt;/p&gt;

&lt;p&gt;Next, on line 7, we set our first Stimulus target with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-target=&quot;comments.commentList&quot;&lt;/code&gt; . We’ll use this target in our Stimulus controller when we insert newly created comments into the DOM.&lt;/p&gt;

&lt;p&gt;Finally, we connect our form to our Stimulus controller with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data: { action: &quot;ajax:success-&amp;gt;comments#createSuccess&lt;/code&gt; on line 23.&lt;/p&gt;

&lt;p&gt;This action listens for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ajax:success&lt;/code&gt; events and calls the createSuccess method when an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ajax:success&lt;/code&gt; event occurs. Remember that, by default, forms created with form_with are remote, which we want. Also note the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-target=&quot;comments.commentBody&quot;&lt;/code&gt; on line 25, which we’ll use in our controller to clear the text field after submission.&lt;/p&gt;

&lt;p&gt;At this point, if we visit a project’s show page in our Rails app and submit our comment form the form submission will work just fine, you will just have to reload the page before you’ll see each new comment.&lt;/p&gt;

&lt;h3 id=&quot;adding-the-stimulus-controller&quot;&gt;Adding the Stimulus Controller&lt;/h3&gt;

&lt;p&gt;Our Stimulus controller is the last piece we’re missing, and it is simpler than you might imagine.&lt;/p&gt;

&lt;p&gt;Let’s start by creating our Stimulus controller:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-console&quot; data-lang=&quot;console&quot;&gt;&lt;span class=&quot;go&quot;&gt;touch app/javascript/controllers/comments_controller.js
  &lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And add in the code we need to process the HTML that Rails sends us each time a comment is successfully POSTed to the server:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&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;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;nc&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;c1&quot;&gt;// Read more about targets in the Stimulus reference guide: https://stimulus.hotwired.dev/reference/targets
&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;commentList&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;commentBody&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;// The connect lifecycle method runs each time our controller is connected to the DOM
&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// Read more about lifecycle methods in the Stimulus reference guide: https://stimulus.hotwired.dev/reference/lifecycle-callbacks
&lt;/span&gt;
  &lt;span class=&quot;nf&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;console&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;We&apos;re connected!&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;nf&quot;&gt;createSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&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;// This might look a little magical but its just deconstructing the standard rails-ujs event detail
&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Read more here: https://guides.rubyonrails.org/working_with_javascript_in_rails.html#rails-ujs-event-handlers
&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;const&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;nx&quot;&gt;_status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;xhr&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;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;detail&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;commentListTarget&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;nx&quot;&gt;xhr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&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;commentListTarget&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;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;commentBodyTarget&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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&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;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;That’s it — that’s the whole Stimulus controller and now we’re handling comment creation without a page turn, like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-07-unstyled-comment-submission.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording showing successful submission of a comment on the form we just built&quot; /&gt;&lt;/p&gt;

&lt;p&gt;How does this all just work? The magic of Rails is doing a lot of heavy lifting behind the scenes so that we can listen for ajax events and respond to them in our Stimulus code.&lt;/p&gt;

&lt;p&gt;Specifically, rails-ujs adds event handlers for us, making it easy to handle ajax events in our JavaScript code. With rails-ujs notifying us of ajax events, Stimulus kicks in through the data-action set on the form so that we can respond to the ajax event(s) we care about and run JavaScript to process the HTML generated by our Rails server. Mostly though, magic.&lt;/p&gt;

&lt;p&gt;If all you were looking for is how to use rails-ujs and Stimulus to handle form POSTs in Rails, you’ve reached the end of the line. Read on for a bonus guide on Tailwind CSS setup and animating our comments.&lt;/p&gt;

&lt;h3 id=&quot;bonus-making-things-fancy-with-tailwind-css-and-animations&quot;&gt;Bonus: making things fancy with Tailwind CSS and animations&lt;/h3&gt;

&lt;p&gt;While adding commenting is nice, everything is more fun when it looks and feels nicer. Let’s add a little style to our project with &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind CSS&lt;/a&gt; and, for fun, look at one way to animate the entry of our comments into the DOM, instead of having them barge in unannounced.&lt;/p&gt;

&lt;h3 id=&quot;adding-tailwind-to-our-project&quot;&gt;Adding Tailwind to our project&lt;/h3&gt;

&lt;p&gt;Tailwind is a wonderful CSS framework that is rapidly gaining popularity in the web development world. While we could add styles without a framework, setting up Tailwind is worth the effort.&lt;/p&gt;

&lt;p&gt;First, let’s install Tailwind and add it to our project, from our terminal:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nx&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tailwindcss&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;javascript&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stylesheets&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;yarn&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;tailwind&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;javascript&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stylesheets&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;tailwind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;js&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;touch&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;javascript&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;stylesheets&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;scss&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Next, in our application.scss file, we need to pull in Tailwind’s base styles:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-scss&quot; data-lang=&quot;scss&quot;&gt;&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;tailwindcss/base&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;s2&quot;&gt;&quot;tailwindcss/components&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;s2&quot;&gt;&quot;tailwindcss/utilities&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Finally, we need to tell webpack about our application.scss file by adding import &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;stylesheets/application&quot;&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/javascripts/packs/application.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since we haven’t seen it before, let’s look at the full application.js file:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;c1&quot;&gt;// This file is automatically compiled by Webpack, along with any other files
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// present in this directory. You&apos;re encouraged to place your actual application logic in
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// a relevant structure within app/javascript and only use these pack files to reference
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// that code so it&apos;ll be compiled.
&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@rails/ujs&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;nf&quot;&gt;start&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;turbolinks&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;nf&quot;&gt;start&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;@rails/activestorage&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;nf&quot;&gt;start&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;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;channels&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;// Uncomment to copy all static images under ../images to the output folder and reference
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// them with the image_pack_tag helper in views (e.g &amp;lt;%= image_pack_tag &apos;rails.png&apos; %&amp;gt;)
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// or the `imagePath` JavaScript helper below.
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// const images = require.context(&apos;../images&apos;, true)
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// const imagePath = (name) =&amp;gt; images(name, true)
&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;controllers&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;stylesheets/application&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Everything in this file is boilerplate generated by Rails when we setup our project except for line 21, which we just added.&lt;/p&gt;

&lt;p&gt;Almost there — now let’s update our postcss file to tell it about our Tailwind configuration file by adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require(&apos;tailwind.css&apos;)(&apos;app/javascript/stylesheets/tailwind.js&apos;)&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postcss.config.js&lt;/code&gt;. Again, let’s take a look at this file in full to make sure we’ve got it setup correctly.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;environment&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;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;nf&quot;&gt;require&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;tailwindcss&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;app/javascript/stylesheets/tailwind.js&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;nf&quot;&gt;require&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;autoprefixer&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;nf&quot;&gt;require&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;postcss-import&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;nf&quot;&gt;require&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;postcss-flexbugs-fixes&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;nf&quot;&gt;require&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;postcss-preset-env&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;na&quot;&gt;autoprefixer&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;flexbox&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;no-2009&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;na&quot;&gt;stage&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;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&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;nx&quot;&gt;environment&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Yours might be a little different — the Rails default is to update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;module.exports&lt;/code&gt; directly instead of assigning the configuration to a variable first. Either way, just add the content of line 3 to the plugins array and you’re all set.&lt;/p&gt;

&lt;p&gt;Next, fire up your webpack-dev-server with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/webpack-dev-server&lt;/code&gt; from your terminal so that changes get compiled as you work.&lt;/p&gt;

&lt;p&gt;Okay — last step before we can start styling things. We need to update the application layout to tell it about the new stylesheet pack that webpack is creating for us by replacing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stylesheet_link_tag&lt;/code&gt; in the default layout with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stylesheet_pack_tag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what your application layout should look like:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;html&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;title&amp;gt;&lt;/span&gt;StimulusComments&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;csrf_meta_tags&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;csp_meta_tag&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;stylesheet_pack_tag&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;application&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;media: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;all&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;data-turbolinks-track&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;reload&apos;&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;javascript_pack_tag&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;application&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;data-turbolinks-track&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;reload&apos;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&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;body&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Now we’re all set to use Tailwind to add a little style to our project. Tailwind’s documentation is exceptional so I’m not going to cover how Tailwind works. Instead, I’ll just add the markup I used, all of which is just standard Tailwind classes.&lt;/p&gt;

&lt;p&gt;Here’s our styled Project show markup:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;container mx-auto max-w-lg border border-gray-200 p-4 mt-4 rounded&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;h2&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-2xl mb-4&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;@project&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;/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;data-controller=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;comments&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;h3&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text-xl mb-2&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Comments&lt;span class=&quot;nt&quot;&gt;&amp;lt;/h3&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;comments.commentList&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mb-4&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;@project&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;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;created_at: :desc&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;comment&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;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;partial: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;comments/show&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;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;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&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;comments/form&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And our comment show partial:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&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;mb-2 border-b border-gray-200 p-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;n&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;/div&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;And, finally, our form:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-erb&quot; data-lang=&quot;erb&quot;&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form_with&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;model: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Comment&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;url: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;project_comments_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@project&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;ajax:success-&amp;gt;comments#createSuccess&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;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;form&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;mb-4&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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text_field&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:body&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;shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline&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;comments.commentBody&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;placeholder: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Type your comment...&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;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;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;submit&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Save&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;bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded cursor-pointer&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;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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;All put together, we get a Project show page that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-07-styled-comment-form.png&quot; class=&quot;centered&quot; alt=&quot;A screenshot of the comment form we&apos;ve built with updated styles added in the last section&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;animating-comment-creation&quot;&gt;Animating comment creation&lt;/h3&gt;

&lt;p&gt;While our comments get added to the DOM right away, they appear instantly, which can feel a little jarring. Let’s wrap up by animating the entry of each new comment into the DOM by first making some small updates to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createSuccess&lt;/code&gt; method in our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;comments_controller.js&lt;/code&gt; file.&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;nf&quot;&gt;createSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;event&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;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;nx&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;xhr&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;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;detail&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;target&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;event&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;c1&quot;&gt;// Create a wrapper div to hold our newly created comment
&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;newComment&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;nf&quot;&gt;createElement&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;div&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;// Add our animation class to the wrapper div and set the innerHTML to the comment rendered by the server
&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;newComment&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;nf&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;fade-in-left&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;newComment&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;nx&quot;&gt;xhr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;response&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// Add our wrapped comment to the beginning of the comment list
&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;commentListTarget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;prepend&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newComment&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;commentBodyTarget&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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;// After the animation finishes (in 0.6 seconds), remove our animation class
&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// If we don&apos;t remove the animation class, adding another comment will cause every comment added since our initial page load to animate together
&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;setTimeout&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;newComment&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;nf&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;fade-in-left&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;mi&quot;&gt;600&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;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;Our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fade-in-left&lt;/code&gt; class isn’t functional yet because we haven’t added any CSS to it. We’ll add that next, with some help from &lt;a href=&quot;https://animista.net/&quot;&gt;animista.net&lt;/a&gt; so we don’t have to write our own animation.&lt;/p&gt;

&lt;p&gt;Let’s add the following to our application.scss file, so we don’t need to create a new file as we near the end of our time together:&lt;/p&gt;

&lt;div class=&quot;code-block&quot;&gt;
  
&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-scss&quot; data-lang=&quot;scss&quot;&gt;&lt;span class=&quot;nc&quot;&gt;.fade-in-left&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;-webkit-animation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fade-in-left&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.6s&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cubic-bezier&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;mi&quot;&gt;.390&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.575&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.565&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;both&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
          &lt;span class=&quot;nl&quot;&gt;animation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fade-in-left&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.6s&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;cubic-bezier&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;mi&quot;&gt;.390&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.575&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.565&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;.000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;both&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;cm&quot;&gt;/* ----------------------------------------------
  * Generated by Animista on 2020-10-15 23:32:49
  * Licensed under FreeBSD License.
  * See http://animista.net/license for more info.
  * w: http://animista.net, t: @cssanimista
  * ---------------------------------------------- */&lt;/span&gt;
  
  &lt;span class=&quot;cm&quot;&gt;/**
  * ----------------------------------------
  * animation fade-in-left
  * ----------------------------------------
  */&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;@-webkit-keyframes&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;fade-in-left&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-50px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                    &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-50px&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;p&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&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;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&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;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;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;@keyframes&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;fade-in-left&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-50px&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
                    &lt;span class=&quot;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;-50px&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;p&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;-webkit-transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&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;nl&quot;&gt;transform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;translateX&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;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;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

  &lt;button class=&quot;copy&quot; style=&quot;text-align: right;&quot;&gt;
  &lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot; style=&quot;height: 1em;&quot;&gt;
    &lt;path stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; stroke-width=&quot;2&quot; d=&quot;M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2&quot; /&gt;
  &lt;/svg&gt;
  &lt;span class=&quot;hint&quot;&gt;Copy&lt;/span&gt;
&lt;/button&gt;

&lt;/div&gt;

&lt;p&gt;With those changes in place, each newly added comment now politely animates into the DOM:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-07-animated-comment-form.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of the comment form. When a comment is submitted, it fades in from the left side of the list as described in the last section.&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;

&lt;p&gt;Together, we looked at how you can use the power of rails-ujs and Stimulus to add a little bit of interactivity to your Rails apps without needing to rely on bulky front-end frameworks or clunky JS partials and jQuery solutions. Stimulus + Rails can deliver simple, clean, and interactive experiences for your users, while retaining all of the joys that come with building standard Rails apps.&lt;/p&gt;

&lt;p&gt;Here’s a preview of what you can build with just a little bit more Stimulus than what we worked on today— all of the interaction you see in the gif below is built with Stimulus + rails-ujs:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/public/images/11-07-animated-drawer.gif&quot; class=&quot;centered&quot; alt=&quot;A screen recording of a web app. A user clicks on a link and a sliding drawer opens from the right side of the screen.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;Additional Resources:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The full code for this repository can be found on  &lt;a href=&quot;https://github.com/DavidColby/stimulus_comments_demo&quot;&gt;Github&lt;/a&gt; .&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://stimulus.hotwired.dev/&quot;&gt;The Stimulus official docs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://guides.rubyonrails.org/working_with_javascript_in_rails.html&quot;&gt;Working with JavaScript in Rails&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.betterstimulus.com/&quot;&gt;BetterStimulus.com&lt;/a&gt;  for various Stimulus patterns and best practices&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Shape Up reflections: Volume 1</title>
   <link href="https://colby.so/posts/shape-up-reflections-volume-1"/>
   <updated>2020-09-28T00:00:00+00:00</updated>
   <id>https://colby.so/posts/shape-up-reflections-volume-1</id>
   <content type="html">&lt;p&gt;&lt;em&gt;Below you’ll find a lightly edited version of an internal update I sent to everyone at CareerPlug after the completing our first product cycle, modeled on Basecamp’s &lt;a href=&quot;https://basecamp.com/shapeup&quot;&gt;Shape Up&lt;/a&gt; method.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Since the target audience for these updates is the company first, I’ll add inline commentary and footnotes as needed to add context for readers that aren’t intimately familiar with CareerPlug’s internal workings.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My intent in sharing these kinds of internal documents is to help demystify the inner workings of product organizations — or at least the one product organization I can demystify.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;As an introduction to our org, CareerPlug has ~50 employees in total, with a product team of 13 people. Today, our product team is made of up two UX designers, eight engineers, and two QA engineers, plus me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our core product, launched in 2012, is an Applicant Tracking System (ATS) used by more than 12,000 businesses in the United States and Canada. Our core clients are brick-and-mortar businesses (think retail, restaurants, and home services) with limited professional HR resources. Our ATS covers everything from distributing job postings to a variety of online job boards (think Indeed) through pre-hire assessments, interviews, offer letters, and background checks. It is a big, fairly complex piece of software, but we do our best to make the end user experience simple.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Let’s dig in. -David&lt;/em&gt;
&lt;!--more--&gt;&lt;/p&gt;

&lt;p&gt;Hi Team,&lt;/p&gt;

&lt;p&gt;Our first product cycle went pretty well!&lt;/p&gt;

&lt;p&gt;We launched new features and learned lessons about how to do better in the next cycle. In the next two cycles we want to keep up the velocity (launching new stuff) and the learning. We know we’ve still got plenty of improvements to make.&lt;/p&gt;

&lt;p&gt;Speaking of learning, in between each cycle I will write reflections on what happened in the previous cycle, what we’ve learned, and what we’re trying next. I’ll share these reflections with the company, for anyone who is interested in knowing about how the product team works.&lt;/p&gt;

&lt;p&gt;Before we start, a note on the content of this update, and what will change in future updates: There isn’t a section below talking about how we’re progressing towards our targeted outcomes. Since none of the work in cycle one has been out in the world long enough to measure the impact, we’re not ready to dig into results.&lt;/p&gt;

&lt;p&gt;In all subsequent versions of these reflections, a section will be dedicated to reviewing the results of work released in previous cycles. We’ll review the expected outcome of each pitch and see if we achieved that outcome.
With that out of the way, here’s this cycle’s David novel 🙂&lt;/p&gt;

&lt;h1 id=&quot;what-worked&quot;&gt;What worked&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;We committed to the right amount of work, and we built the right processes to deliver those commitments.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Of the projects we committed to in this cycle, we completed all but one.&lt;/p&gt;

&lt;p&gt;Delivering almost everything we wanted to in our first cycle is a big win, and I’m proud of how well we performed as a team. We pushed ourselves to find ways to solve problems in the allotted time, made hard decisions about scope, and mostly succeeded in producing what we set out to produce.&lt;/p&gt;

&lt;p&gt;I’ll talk more about learnings from the project we didn’t deliver in just a bit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product-team communication was strong&lt;/strong&gt;
We’ve talked before about rebuilding the airplane while we’re still in the air - we did that successfully in this cycle. Engineers and designers had to decide on and implement new ways to communicating with each other as we moved from high-fidelity, finished mockups to “here’s a problem and some words about a solution, let’s figure out what works.”  That’s hard to do.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[David note: Our pitches are a little different than what is outlined in Shape Up. Mostly, our pitches leave more space available for UX and engineering to design the solution. My version of fat marker sketches is words, most of the time.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each project team handled the communication problem differently but they all arrived at a solution that worked. They kept each other informed, solved problems, got feedback, and released high-quality work under tight deadlines.&lt;/p&gt;

&lt;p&gt;Beyond the project team level, multiple people on the product team flagged process gaps and problems that need to be fixed in the next cycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-team feedback happened&lt;/strong&gt;
We got more feedback and insights from CX and sales than we’ve ever gotten before. The projects we completed this cycle are better due to that participation. This feedback was possible thanks to a concerted effort from project teams to connect with other teams and keep them involved.&lt;/p&gt;

&lt;p&gt;A lot of work went into trying new ways of communicating with other teams. Connecting when schedules are full and case loads are high is difficult. What we tried was mostly successful, compared to what we’ve done before.&lt;/p&gt;

&lt;p&gt;There’s more work to do to keep these channels open and make sure everyone who wants to has a chance to share their thoughts, but we should celebrate our progress here.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[David note: A major driver in switching to Shape Up was seeing an opportunity to open up the pitch process for feedback across the company and to push UX and engineering to work more directly with other teams.
In our previous process UX was getting business requirements and getting some feedback during design but engineering would often change implementation details during development. These surprise changes made our feedback processes feel insufficient. Shape Up isn’t really a required thing for this, but I saw a way to adjust the process a bit to meet this need for my team.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The investment we made in Voyager is paying dividends&lt;/em&gt;
Bear with me on this one, I know that we’ve still got plenty of work to do on Voyager, especially on the settings pages.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[David note: Voyager is the name of our redesigned ATS. We made a decision ~18 months ago that we needed to give ourselves a better base to build from, and Voyager is what we’ve built. Voyager has been in use by clients since March, and the bulk of our client base uses Voyager exclusively. Some clients still use the original version of our ATS, which we call Classic. In this section, I spend time reminding the team why we took on this work — which has been contentious at times due to mistakes I’ve made along the way — because repetition helps with important messages like this.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’m going to briefly recap the Why of Voyager to frame up what I mean by paying dividends.&lt;/p&gt;

&lt;p&gt;We invested in Voyager because we knew the ATS had reached a point where we could no longer innovate on the product in meaningful ways. It worked well enough and people who knew how to use it got by just fine, but that was about it. The most obvious sign that something was wrong was that most of our core features were used by a small percentage of our users.&lt;/p&gt;

&lt;p&gt;The years we spent searching for our identity as we grew beyond just serving State Farm led to a lot of unused features, confusing experiences, and a UX that couldn’t accommodate any new workflows. Now that we finally knew exactly who the product was for, we had to get the product into a state where it could actually be made for those people.&lt;/p&gt;

&lt;p&gt;On the technical side, we did an admirable job keeping the code base clean and the product performant with the resources we had in the early years. &lt;em&gt;[David note: We had one engineer for the first few years of our life, and 2 for a few years after that.]&lt;/em&gt; We built automations and wrote tests when we could, and we survived. Things were okay, and new developers were able to get up and running without /too/ much pain. Just like for our clients, our developers and designers experience with the ATS was, at best, fine.&lt;/p&gt;

&lt;p&gt;Despite all of our hard work, a decade is a very long time in web development years, and we have learned a lot about how to build fast, scalable products in the last ten years. We knew our architecture needed to change to get where we wanted to go. &lt;em&gt;[David note: On the technical side we primarily needed to rip out much of our frontend architecture, which was pretty out of date. We also upgraded to Rails 6 and started using Stimulus.js heavily]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Some of the features and experiences we want to build might have been possible on Classic, but they wouldn’t be the best they could be. Staying on Classic would have locked us into innovating slowly on a less reliable, slower product. With a stronger technical foundation, we would have the ability to innovate faster while making the product faster, more reliable, and easier to understand.&lt;/p&gt;

&lt;p&gt;The right long term investment was building Voyager. We know who our product is for, and how to get it to them, and with Voyager, we knew we’d have a product ready for long term growth. In the short term, that meant a whole lot of pain and, until now, not much to show for the work.&lt;/p&gt;

&lt;p&gt;This product cycle, Voyager began to pay us back. We have a strong technical foundation, a well-maintained codebase that follows internal best practices and agreed upon patterns, and a library of components we can use to quickly implement new features. The feature work we did in this cycle all moved faster because of those benefits, none of which could have existed in Classic.&lt;/p&gt;

&lt;p&gt;On top of the speed of innovation, we also saw the benefit of having space to add new features and functionality. There’s room to grow, thoughtfully, which means we can build the things our users need. Even better, having space means we can show our users that these new features exist and educate them on how they work. Classic was (and is) so full of stuff — much of it unused— that we didn’t have room to educate our users and help them succeed. New features in Classic just fell into the abyss.&lt;/p&gt;

&lt;p&gt;We aren’t done with Voyager — and the ATS will never be “done” — but this cycle was the culmination of eighteen months of hard work, and the early results are exactly what we envisioned when we started building.&lt;/p&gt;

&lt;h2 id=&quot;what-didnt-work&quot;&gt;What didn’t work&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Four week cycles and one week projects&lt;/strong&gt;
When we decided to move to this new cycle process, I made the call to make cycles four weeks long. I made this decision because moving straight from two week sprints to six week cycles felt like too big of a leap. I was too cautious here. We should have just ripped off the bandaid and gone straight to six weeks.&lt;/p&gt;

&lt;p&gt;Four week cycle length was the wrong decision because four weeks is too cramped. Projects aren’t just engineering work - research, UX design, prototyping, and feedback all have to happen alongside engineering and QA. Doing all that well inside of a four weeks is always going to be very hard, and sometimes it will be impossible.&lt;/p&gt;

&lt;p&gt;Even worse, four week cycles led us to include several one week (and two half week) projects. One week is way too tight for proper research, design, engineering, and QA, even for small projects.&lt;/p&gt;

&lt;p&gt;We made it work this cycle because of a lot of hard work from the team, but four week cycles and one week projects don’t allow us to bring our best.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feedback from clients was too light&lt;/strong&gt;
We didn’t get much client feedback on our larger projects. This is a function of two things:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;We don’t have a strong bench of readily available clients to give feedback without a lot of advance notice. Scheduling a call two weeks out doesn’t work in our new world where we’re designing and developing concurrently and releasing quickly.&lt;/li&gt;
  &lt;li&gt;Four week cycles don’t leave much time for feedback at all. We need to start building really quickly. With six weeks, we’ll have a little more time early on to get organized and connect with clients.
Without more direct client feedback on larger projects, we’ll miss the mark more often than we should.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-we-learned&quot;&gt;What we learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;This new process can work!&lt;/strong&gt;
Launching a new thing that changes how a whole company works together is hard. There’s always a risk that it won’t work, even if it sounds great on paper.&lt;/p&gt;

&lt;p&gt;In this case, it went pretty well. We worked together, we moved the product forward, and we learned how we can do even better next time. We know the core philosophy is sound, and that we can do good work within our new constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We need to be better partners — and ask for more from our partners&lt;/strong&gt;
Integration work that follows our new process rules (fixed, non-negotiable timeframes) is going to be tough. We had two point of hire integrations slated for this cycle and only one of them got done. The integration we missed on was identified as a risk on day one. We know they would likely be slow to respond, and we tried to get ahead of it by reaching out proactively very early on, but we didn’t reach the finish line.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[David note: We have a variety of integrations with external systems (background check providers, payroll systems, time tracking systems). We’re still learning how to best work with these partners in a way that doesn’t cause a lot of extra operational overhead.]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We need to be better partners by communicating openly about how our process works and what we require from partners before and during development cycles. Misalignment of expectations with an integration partner is very costly for us because we bear all of the development cost. It doesn’t cost the partner anything if a project drags on, which means our risk going into a new integration partnership is much higher than the partner’s risk.&lt;/p&gt;

&lt;p&gt;We shouldn’t commit until we’ve gotten buy-in that the partner is there to support us from the start, including a commitment to have a technical contact who can be available within 24 hours when building and testing the integration.&lt;/p&gt;

&lt;h2 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;New cycle and project lengths&lt;/strong&gt;
In the next cycle, we’ll learn how six week cycles work for us.
Six week cycles means more time for research, testing, and feedback, and will give our ideas a little more room to grow. Each six week cycle will be followed by a two week cooldown - one extra week for devs to improve our infrastructure, tooling, and dive into creative ways to be more efficient and deliver a great product. Cooldown means plenty of bug fixing time too.&lt;/p&gt;

&lt;p&gt;We’re coupling six week cycles with a minimum project length of two weeks. If we’re truly following our new process, even small development projects need time for research, testing, and feedback, along with writing code and doing QA. One week isn’t enough time to do all of that well for anything more than the smallest of features. Two weeks feels right, right now.&lt;/p&gt;

&lt;p&gt;A minimum of two weeks for small batch projects means we’ll have to be more thoughtful about how we construct pitches for two week projects. There are always lots of incremental improvements that can be made to a product, and that’s especially true of our ATS since we’re still completing a major redesign. Classic has plenty of good ideas that have yet to make it to Voyager, and those good ideas are often very small tweaks that, on their own, aren’t two weeks of work.&lt;/p&gt;

&lt;p&gt;Our job will be to find the themes in the small tweaks that we need to make so that they can be combined into coherent, measurable small batch projects. These themes (say, making filtering on the applicants page better) will help us make the small, incremental changes we need to make without letting small batch projects devolve into free-for-alls that can’t be reflected on or measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retain&lt;/strong&gt;
You’ll see the first work on Retain happening in this cycle. By the end of the cycle, you’ll be able to see the first pieces of Retain and use them yourselves.&lt;/p&gt;

&lt;p&gt;While that work happens, Clint and I will continue to refine the vision for Retain and recruit advocates for Retain from outside of the company to be our early testers and to serve as champions for Retain when it is ready for the world to see.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[David note: Retain is a new product we are building. It will release in 2021]&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communication&lt;/strong&gt;
We’re still learning how best to communicate — with each other, with the company, and with clients — and the next cycle will be another opportunity to grow. Expect more opportunities to give preview in-progress work, more visible product metrics, and better insight into upcoming pitches.
We’ll also continue working on building our bench of go-to clients for feedback and testing. Longer cycles will give us more time to do that, rather than constantly being heads down trying to hit a delivery deadline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product launches&lt;/strong&gt;
Now that we know who we’re building for, and we’re building things that benefit all of our users, we can begin talking more publicly about /what/ we’re launching and /why/ people should care. We’ll be experimenting with new ways to educate users and prospects about what’s changed in CareerPlug, and how what we’re building will benefit their business.&lt;/p&gt;

&lt;p&gt;We’re proud of the work we’re doing, and we know it provides enormous value for thousands of companies. We’re going to start bragging about it a little more, and pushing people to use everything in the system, not just the stuff we’re not scared to tell them about.&lt;/p&gt;

&lt;p&gt;That’s all for cycle one. Thanks for reading.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Product managers: your job is not to say no</title>
   <link href="https://colby.so/posts/your-job-is-not-saying-no"/>
   <updated>2020-07-13T00:00:00+00:00</updated>
   <id>https://colby.so/posts/your-job-is-not-saying-no</id>
   <content type="html">&lt;p&gt;One of the most damaging pieces of advice you can receive as a new product manager is that &lt;a href=&quot;https://www.intercom.com/blog/product-strategy-means-saying-no/&quot;&gt;your job&lt;/a&gt; is to say no. This advice, well-meaning and oft-repeated, isn’t wrong but when taken at face value it is incredibly damaging.&lt;/p&gt;

&lt;p&gt;Your job as a product manager is not saying no; your job is saying yes to the right things.&lt;/p&gt;

&lt;p&gt;This might seem like a silly distinction — in either case, you’re saying no a lot, right? — but your mindset makes all the difference.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h3 id=&quot;how-you-perceive-the-idea&quot;&gt;How you perceive the idea&lt;/h3&gt;
&lt;p&gt;When you define your core function as saying no, you approach every new idea closed off, ready to fire off a no, especially if what you are hearing doesn’t align with the plans you already have. When your job is to say no, you look for reasons to throw an idea onto the no pile so you can check the box — “Great job, you did your job and protected the product!” — without giving each new piece of information the right level of consideration.
Each time you say no, you should be able to articulate why you’re saying no — to yourself and to anyone around you.&lt;/p&gt;

&lt;p&gt;If you can’t articulate why it’s a no, you are not ready to say no.&lt;/p&gt;

&lt;p&gt;Forcing yourself to articulate why you’re saying no will help you build better products. You never know where the next great idea will come from. If you are tuning in to a variety of &lt;a href=&quot;https://medium.com/the-innovation/how-to-know-where-to-go-directional-inputs-for-your-product-8c1a700311cf&quot;&gt;directional inputs&lt;/a&gt; you are going encounter new ideas constantly but these new ideas are useless if your mental model doesn’t allow you to approach each idea with an open mind.&lt;/p&gt;

&lt;p&gt;This shift in perspective — from starting at no to starting at why not — will have a profound impact on how you perceive new ideas. Each new idea becomes an opportunity to learn and expand your understanding of your product’s place in the market instead of another distraction from your goals.&lt;/p&gt;

&lt;h3 id=&quot;how-others-perceive-you&quot;&gt;How others perceive you&lt;/h3&gt;
&lt;p&gt;“I know you’re going to say no to this but I have to ask you anyway.”
A &lt;a href=&quot;https://hbr.org/2019/11/what-is-a-customer-success-manager&quot;&gt;CSM&lt;/a&gt; started a conversation with me by telling me what I was going to say before I said it. That sentence opened my eyes to how my approach to internal conversations was making others feel. Focusing on saying no — protecting the roadmap instead of hearing and considering each new idea — caused others to feel unheard. Despite wanting to give everyone an opportunity to make their voice heard, my approach to conversations with my internal team made them feel left out.&lt;/p&gt;

&lt;p&gt;When people come to you, the product manager, they’re bringing problems and ideas that matter to them and they want to know that they matter to you too. Leading with no means that you won’t engage deeply with the idea in front of you, and others will pick up on that energy. They’ll get tired of hearing no — even if you let them talk before you say no — and eventually they’ll be too discouraged to bring new ideas to you since they expect you to shut their idea down.&lt;/p&gt;

&lt;p&gt;If you start each conversation by looking for the right things to say yes to, you create an environment where others feel heard. They’ll feel you engaging with the idea, digging in deeper, asking why, and encouraging them to think through the idea themselves.&lt;/p&gt;

&lt;p&gt;This shift makes a big difference in the tone of a conversation. More questions get asked as you take the time to learn more about the problem they need to solve. You interrogate your current assumptions and share those assumptions with the person you’re talking to. They learn from you as you talk to them about how the idea they have might fit into the overall product strategy, and you learn from them as they bring you new information. If the conversation ends in a no, the right mindset can soften the blow that comes with hearing no.&lt;/p&gt;

&lt;p&gt;This might sound a lot like being a good listener which, yes — true. But product managers with great listening skills don’t always deploy those skills equally. Some PMs will use their strong listening skills when talking with someone who has a high level of influence in the organization or on the roadmap. When those same PMs are talking with someone with little or no influence, their great listening skills disappear, replaced by a race to say no and move on.&lt;/p&gt;

&lt;p&gt;Good ideas can come from anywhere, not just from the top. If you approach each product conversation as a chance to learn, grow, and maybe uncover a great new idea, you’ll build stronger relationships and a better product.&lt;/p&gt;

&lt;h3 id=&quot;thoughtfully-saying-no&quot;&gt;Thoughtfully saying no&lt;/h3&gt;
&lt;p&gt;So you’re saying I’m supposed to say yes to more things? No (ha). You are still going to say no a lot. I’m not arguing that saying no is bad, instead, I’m encouraging you to think about the impact your mindset has on you, your users, your peers, and your product.&lt;/p&gt;

&lt;p&gt;Remember that your job is to say yes to the right things. You are tasked with deploying your limited resources to delivering the most value to your customers. Each time you encounter a new idea, look at that as an opportunity to learn and approach the conversation with a goal of discovering if the idea is the right thing for your product. If it isn’t, thoughtfully say no by keeping these key points in mind:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Be clear — if the answer isn’t yes, the answer is no. Not “not right now” or “maybe later”. Just say no.&lt;/li&gt;
  &lt;li&gt;Explain your reasoning — why isn’t this the right thing to work on right now? What are we saying yes to and why? Give context so everyone understands why the answer is no.&lt;/li&gt;
  &lt;li&gt;Acknowledge the impact — the idea got to you because someone cared about it. Hearing no is disappointing at best and in the worst cases, it can cost you business. Have empathy for the people who have to hear (and deliver) the no.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Do less: A strategy for building better products</title>
   <link href="https://colby.so/posts/do-less"/>
   <updated>2020-07-05T00:00:00+00:00</updated>
   <id>https://colby.so/posts/do-less</id>
   <content type="html">&lt;p&gt;Product managers love building new things. It is a driving force behind why many of us decide on this career path; we get to spend all day figuring out what cool new thing to build next.&lt;/p&gt;

&lt;p&gt;This love of building stuff serves us well in the early days of a product. Everything is new, you don’t have many users, and you are missing core components of your product’s value proposition. Delivering new features quickly helps you find problems that your users want you to solve before you run out of cash.&lt;/p&gt;

&lt;p&gt;Eventually, if you build the right things and all of the pieces fall into place, you get past the early stage of searching for product-market fit and find that you have a growing base of happy users.&lt;/p&gt;

&lt;p&gt;Reaching this point is a wonderful achievement — and a signal that your product strategy needs to change.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;new-feature-addiction&quot;&gt;New feature addiction&lt;/h2&gt;
&lt;p&gt;Many product teams fail to recognize the strategic shift they need to make once they reach product-market fit and so they continue churning out new feature after new feature for years without knowing why. Often times this happens out of inertia. We’re used to building new things and building new things is comfortable, so we keep building new things.&lt;/p&gt;

&lt;p&gt;Features will be released, fail to catch on, and drift off into the abyss as your team pivots to the next new idea. This inertia, left unchecked, will lead to a confusing product that lacks vision and fails to deliver on its promise.&lt;/p&gt;

&lt;p&gt;All of these new features, each of which was probably a nice idea in isolation, will begin to wear on users. They bought your product because it solved a problem for them and each new feature release is another thing they have to learn, a new tab, a new button, a new pop-up announcing the latest release that they don’t need.&lt;/p&gt;

&lt;p&gt;New feature addiction will eventually produce outcomes that you aren’t going to like:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Your core feature set will age poorly. As you chase new features to expand your product and solve new problems, the features that got you to product-market fit will start to feel less valuable, less innovative. Your competitors will learn what made you successful and they’ll copy and improve on the good parts, without all of your baggage.&lt;/li&gt;
  &lt;li&gt;The debt built up from years of new features that didn’t quite hit the mark will make your product hard to use, hard to explain, and weigh down your engineers. Bugs will crop up more frequently, your support team will struggle to articulate how your product works, and your velocity will slow down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid these outcomes by intentionally doing less.&lt;/p&gt;

&lt;h2 id=&quot;do-less&quot;&gt;Do less&lt;/h2&gt;
&lt;p&gt;Do less doesn’t mean stop working on your product. Instead, it is a call to refocus your team on the right work to keep your product healthy for the long term. A less controversial way to think about the concept of doing less is doing less work that people see.&lt;/p&gt;

&lt;p&gt;What I really mean when I encourage product teams to “do less” is to make your product better by focusing on the hard, often invisible work of optimizing what you already have. If you do this right your users will /feel/your work more than they’ll see your work.&lt;/p&gt;

&lt;p&gt;Doing less can feel uncomfortable, frustrating, or confusing for teams that are used to feature-based roadmaps and cultures that praise output and splashy new releases.&lt;/p&gt;

&lt;p&gt;How can you do less and still deliver a great product? Let’s find out.&lt;/p&gt;

&lt;h2 id=&quot;know-who-you-are&quot;&gt;Know who you are&lt;/h2&gt;
&lt;p&gt;The first step to doing less is to create a  &lt;a href=&quot;https://www.romanpichler.com/blog/tips-for-writing-compelling-product-vision&quot;&gt;product vision&lt;/a&gt;  that clearly defines what your product is, who it is for, and what problems it solves. This vision will form the boundaries within which changes to the product should live. If a feature idea comes through that sounds valuable but doesn’t align with the vision, that feature should not be built.&lt;/p&gt;

&lt;p&gt;Before you achieve product-market fit, you should have a product vision, but that vision likely won’t have enough details to keep new feature addiction in check. As you turn the corner to a more mature product, refine your vision, get buy-in from your team, and build processes to hold yourself accountable to the vision.&lt;/p&gt;

&lt;p&gt;Do less by defining your vision and boxing out the work that distracts you from achieving that vision.&lt;/p&gt;

&lt;h2 id=&quot;go-deep-not-wide&quot;&gt;Go deep, not wide&lt;/h2&gt;
&lt;p&gt;Besides inertia and a love of the new, another reason we build new features is that new features are easy to build. Every new feature is a Greenfield full of hopes and dreams for what it could be in — all the hard stuff is still somewhere out ahead.&lt;/p&gt;

&lt;p&gt;Working to improve a feature after it is out in the world is more difficult. You have to go deep into how people use the feature, what value they get from it, and how it could be improved. This type of work uncovers more warts than you’ll find in Greenfield work and it carries with it more risk of upsetting your users if you miss the mark.&lt;/p&gt;

&lt;p&gt;Despite the difficulty of the work, going deeper into the features that your users already value will produce a higher return in the long run. Your customers didn’t buy 100 features, they bought a handful of really valuable, easy-to-use features. Continuous improvements to a small set of high-value features will always win out over a constant barrage of new features that are tangential to your product’s core job.&lt;/p&gt;

&lt;p&gt;Do less by going deep into your core features, instead of playing around on the margins.&lt;/p&gt;

&lt;h2 id=&quot;focus-on-quality&quot;&gt;Focus on quality&lt;/h2&gt;
&lt;p&gt;No matter what your software does and who your users are, there are two things that your customers want:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Your product should be fast&lt;/li&gt;
  &lt;li&gt;Your product should work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those two things are universally true in software. No one wants to use your slow product, and they don’t want to deal with bugs.
Let’s tackle speed first. We  &lt;a href=&quot;https://www.cloudflare.com/learning/performance/more/website-performance-conversion-rates/&quot;&gt;know&lt;/a&gt;  that faster page loads produce better conversion rates — people don’t want to waste time staring at a blank screen or a loading icon.&lt;/p&gt;

&lt;p&gt;Speed typically isn’t a concern in the early days of most SaaS products. Everything is fast because no one uses your product. Problematic database queries are masked behind tiny tables, edge cases haven’t been discovered yet, and your single Heroku instance can handle 100x your peak traffic.&lt;/p&gt;

&lt;p&gt;Left unattended, your product will slow down. Database tables grow, edge cases become commonplace, and your eyes are on launching a new feature while your users spend 3 seconds waiting for every single page turn. Time spent optimizing performance will almost always be a higher return on investment than time spent building nice-to-have features. Don’t wait until your users tell you things are slow — push your engineering team to proactively seek out and eliminate poor performance early and often.&lt;/p&gt;

&lt;p&gt;The other side of quality is fixing bugs. Software is never bug-free and that  &lt;a href=&quot;https://m.signalvnoise.com/software-has-bugs-this-is-normal/&quot;&gt;should not be the aim&lt;/a&gt; ; however, a perception that your product is “buggy” will kill your growth faster than nearly anything else. No one wants to spend their time and money on something that doesn’t work — error pages, downtime, and unexpected behavior all add up to a terrible experience. Ignoring bugs will encourage your users to move on to a competitor that can promise a more reliable solution, even if your product has more features.&lt;/p&gt;

&lt;p&gt;The cost of unresolved bugs compounds over time. A bug is a crack in your product’s foundation — the best time to fix that crack was before it existed. The second best time is now. Find and fix bugs before they reach your users, and build a product that is focused, testable, and easy to understand, or find yourself paying  &lt;a href=&quot;https://azevedorafaela.com/2018/04/27/what-is-the-cost-of-a-bug/&quot;&gt;the cost&lt;/a&gt;  for a buggy product.&lt;/p&gt;

&lt;p&gt;Do less by prioritizing a fast, reliable product experience.&lt;/p&gt;

&lt;h2 id=&quot;doing-less-doesnt-mean-stop-innovating&quot;&gt;Doing less doesn’t mean stop innovating&lt;/h2&gt;
&lt;p&gt;Does doing less mean you never build new features?&lt;/p&gt;

&lt;p&gt;No. There will be times when an opportunity exists that is worth investing in. If you stay focused on doing less, you’ll be more likely to be in a position to capitalize on new opportunities because your existing product will be more focused and more prepared to integrate a new core feature than an unfocused product full of unfinished ideas.&lt;/p&gt;

&lt;p&gt;How do you know if a feature is worth investing in? Start with your product vision. Does the new feature closely align with your vision? Do the users you serve have the problem you want to solve and do they trust you to solve it? Do you have enough  &lt;a href=&quot;https://medium.com/the-innovation/how-to-know-where-to-go-directional-inputs-for-your-product-8c1a700311cf&quot;&gt;input signals&lt;/a&gt;  to validate that the idea is worth pursuing? If you can answer yes to these questions, and your core features are fast, reliable, and well-optimized, then it might be time to incorporate something new.
￼&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;Doing less is hard — I’ve struggled at various times in my career with wanting to push new features ahead of the less exciting and less visible work that comes with committing to doing less.&lt;/p&gt;

&lt;p&gt;The strategies I’ve outlined above have helped me keep my teams focused on the long term, and they’ve been developed in direct response to getting off course by chasing after new things instead of digging deeper.&lt;/p&gt;

&lt;p&gt;The work of doing less requires discipline, focus, and clarity of purpose. As with most really valuable things, you won’t get there overnight but the results will be worth it.&lt;/p&gt;

&lt;p&gt;If you’re struggling with where to start, try to identify just one area where you can start to change your habits:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;If you’re dealing with a buggy or unreliable product, work with your engineering team to understand what’s causing the bugs and divert resources away from new feature development to resolve outstanding bugs&lt;/li&gt;
  &lt;li&gt;Look for an area of your core product that you haven’t touched in a while. Use that feature as if you were a new user and take notes on what you could improve about the experience. Spend time reviewing your directional inputs to find patterns related to that core feature. Develop a plan to make that one feature a bit better than it is today and see how that work impacts your KPIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting small, changing what you can, and looking for evidence that this approach works for your product, is, as usual, the best approach.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Directional inputs for your product</title>
   <link href="https://colby.so/posts/directional-inputs-for-your-product"/>
   <updated>2020-06-29T00:00:00+00:00</updated>
   <id>https://colby.so/posts/directional-inputs-for-your-product</id>
   <content type="html">&lt;p&gt;The world of product management is noisy. Your clients are asking for new features. Your sales team needs a magical new feature to close their next deal. Your success team heard from 50 people yesterday about gaps in your product. Noise is coming in from every direction, all of the time.
Learning to process a constant barrage of information from disparate sources into a coherent product vision is vital to success as a product manager.&lt;/p&gt;

&lt;p&gt;One mistake that product managers make is becoming too focused on a single input channel. Most often, this happens when a single source is the loudest — the squeaky wheel gets the new feature. To break out of this habit, it can help to identify all of the different input channels that you should be monitoring and clarify the role each channel plays in the product management process.
&lt;!--more--&gt;&lt;/p&gt;
&lt;h2 id=&quot;external-conversations&quot;&gt;External conversations&lt;/h2&gt;
&lt;p&gt;Let’s talk about external conversations with users and prospects first. This is the lowest hanging fruit — we all know we’re supposed to be talking to people, right? Right.&lt;/p&gt;

&lt;p&gt;Early on in a product’s lifecycle direct conversations with the people who use your product and the people who might use a product like yours are your lifeblood. If you haven’t achieved product-market fit, you’ll never get there without talking to your users and learning from them about how to make your product better. You don’t yet have enough users to have reliable or useful data so you must make judgment calls based on your deep understanding of the individuals who use your product. Trusting your gut is a lot easier if you have a bench of people who can provide honest feedback for you as you shape the early direction of your product.&lt;/p&gt;

&lt;p&gt;As your user base grows, the weight you give to each individual opinion from a user must change. In the early days, you may only have 100 users — you can talk to 10 of those people and feel pretty confident you know how your user base feels about a particular topic. Once you have thousands or millions of users, counting on one-to-one conversations as your primary source of truth becomes less useful.&lt;/p&gt;

&lt;p&gt;External conversations will always be a key input channel as a product manager. No matter the size of your user base you can use these conversations to rapidly identify problems and new opportunities and to get quick, honest feedback on your product. External conversations also play an important role in humanizing your users — telling a story about a real person when you are &lt;a href=&quot;https://medium.com/@davidcolby/how-to-pitch-feature-ideas-to-a-product-manager-cb5e0add1a6c&quot;&gt;pitching an idea&lt;/a&gt; is a great way to build a case to a reluctant internal team.&lt;/p&gt;

&lt;h2 id=&quot;internal-conversations&quot;&gt;Internal conversations&lt;/h2&gt;
&lt;p&gt;Internal conversations with your frontline teams help you identify trends, pressure test ideas and multiply your access to real stories. Your support team members are going to know your product better than you, especially the quirks that confuse or frustrate your users. Your sales team will know what features really get people excited and which are falling flat in demos. Talking to these frontline teams regularly will help you identify the areas of your product that need the most focus. Building trust and two-way communication with these frontline teams is one of the best things you can do to keep your product heading in the right direction.&lt;/p&gt;

&lt;p&gt;Conversations with your engineering team are part of this channel as well. Keep an open line of communication with your key engineering resources and leave room for them to share honest feedback with you on what technical debt needs to be addressed and when.&lt;/p&gt;

&lt;p&gt;Internal conversations can be a dangerous input channel for a product manager because of the dreaded &lt;a href=&quot;https://www.forbes.com/sites/bernardmarr/2017/10/26/data-driven-decision-making-beware-of-the-hippo-effect/#3a333d9f80f9&quot;&gt;HiPPO&lt;/a&gt; trap. The pressure to move a pet project along from an executive can be intense. It can sometimes feel easier to just prioritize a project to eliminate the pressure, even when you know that it isn’t the right direction for your product. Managing expectations and avoiding the trap is a topic for another day — for now just keep in mind that when you approach internal conversations with executive teams, your job as a product manager is to deliver a great product and it is your role to speak up and push back when steps are being skipped to ease internal pressure.&lt;/p&gt;

&lt;p&gt;Conversations, whether they’re with internal or external folks, will be happening daily and this input channel should always be the biggest block on your calendar. Avoid spending all of your time in meetings and on the phone with users and internal stakeholders— this channel can be incredibly time consuming and you will find that the return on most individual conversations is quite low.&lt;/p&gt;

&lt;h2 id=&quot;research&quot;&gt;Research&lt;/h2&gt;
&lt;p&gt;Research is an input channel that encompasses all of the stuff that product managers do that looks like searching Google all day. Competitor research, industry research, market research, and, depending on your company’s definition of product management, technical research all get broadcast on this channel. This channel must be actively sought out — your competitors aren’t going to bang down your door to tell you about a new feature and you are probably not so central to your industry that the news all runs through you.&lt;/p&gt;

&lt;p&gt;The most obvious input in this channel is competitor research. Be cautious about relying too much on competitor research — in many cases, you’ll find it is of limited value. Don’t ignore what your competitors are doing but if what they are doing is driving your roadmap you will always deliver a product that feels a little bit behind. Instead, keep an eye on what they’re doing and know how you compare but build what works best for your users and your own long term goals.&lt;/p&gt;

&lt;p&gt;Because you have to actively seek out this channel, it is easy to ignore it in favor of channels that demand your attention. To avoid this, I recommend carving out dedicated time to focus on researching what’s happening in your space and writing up notes to structure your thinking. The right cadence for this type of work is determined by your working style and the maturity of your product and your market — a few hours a week works best for me but you might find you need more or less time on this channel.&lt;/p&gt;

&lt;p&gt;A handy way to save yourself some time and energy when doing research is to let other people do some of the work for you — subscribe to a service like &lt;a href=&quot;https://owler.com/&quot;&gt;Owler&lt;/a&gt; to rollup news on industries or competitors you care about, find the important newsletters and journalists covering your industry and subscribe to them, and let the research show up in your inbox.&lt;/p&gt;

&lt;h2 id=&quot;feedback-data&quot;&gt;Feedback data&lt;/h2&gt;
&lt;p&gt;Feedback data is our first of two channels that involve that famous product management task of staring at a spreadsheet for a few hours and wondering why no one likes the feature everyone told you they had to have.&lt;/p&gt;

&lt;p&gt;Feedback data is aggregated information on what clients are saying about your product. We’re firmly in qualitative data land with this channel and most of the value in this channel will be in reading words that people wrote about your product.&lt;/p&gt;

&lt;p&gt;This type of data can be sourced from places like:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Client surveys (such as NPS)&lt;/li&gt;
  &lt;li&gt;Support cases from your ticketing system&lt;/li&gt;
  &lt;li&gt;Search trends on your online help center&lt;/li&gt;
  &lt;li&gt;Feedback on a community portal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feedback that you have available will depend on the size and maturity of your business, how transparent individual business units are, and what proactive work you’ve done to build these channels.&lt;/p&gt;

&lt;p&gt;Feedback helps you apply a new lens to individual conversations — if you hear a problem in a one-on-one conversation your next step should often be referencing aggregated feedback to look for validation this the problem is relevant across your user base.&lt;/p&gt;

&lt;p&gt;Feedback can also help you identify trends that you might not see in a few individual conversations. NPS surveys and other sources of direct, structured feedback on your product from a wide range of users can identify the most acute problems your product has and, with the right analysis, can help surface potential solutions to those problems.&lt;/p&gt;

&lt;p&gt;In my experience, &lt;a href=&quot;https://www.netpromoter.com/know/&quot;&gt;NPS data&lt;/a&gt; is often the most valuable input in this channel. NPS surveys are a constant source of high-value ideas and, as your product matures, you’ll find that negative NPS feedback about your product will focus on very similar things. If you aren’t running NPS surveys for your product, consider partnering with your client-facing teams to start an NPS process as soon as you can. Every zero you receive is an opportunity to have a conversation about what your product can do better.&lt;/p&gt;

&lt;p&gt;If you have access to it, support case data can be high impact too. Reading email/chat cases directly is the ideal for this, but even higher level trends on the types of questions people ask can be a useful source for understanding what areas of your product you should focus on.&lt;/p&gt;

&lt;p&gt;The right cadence for reviewing and analyzing feedback will depend on the data that you can access and how much of it you have. In most cases, a weekly review of incoming data will work. In organizations with longer cycles or lower volume, a monthly deep-dive may be sufficient.&lt;/p&gt;

&lt;h2 id=&quot;engagement-data&quot;&gt;Engagement data&lt;/h2&gt;
&lt;p&gt;This is where the spreadsheets can get really intense (this is how I learned Excel has a row limit of 1,048,576) and where you can get very, very lost if you aren’t careful.&lt;/p&gt;

&lt;p&gt;Engagement data moves away from softer, qualitative feedback data into direct quantitative data. Forget what people are saying about your product — how are they actually using it?&lt;/p&gt;

&lt;p&gt;Engagement data plays two important roles in helping you shape your product’s direction.&lt;/p&gt;

&lt;p&gt;First, engagement data allows you to validate what you are hearing through conversations and feedback inputs with how people are actually using your product. Is everyone telling you that they love a new feature? That’s great! How many of them have used it more than once?&lt;/p&gt;

&lt;p&gt;Whenever possible, back up decisions that you make based on conversational input with usage data.&lt;/p&gt;

&lt;p&gt;Second, the vast majority of your users are silent. You never hear from them and you never will. They aren’t going to get on the phone with you, contact your support team, or fill out a survey. They’ll just use your product in silence. The only way you can give these users a voice is to understand how they use your product. Which features are popular and which are lagging? Which features are most likely to predict renewal? Which features never get used but feature prominently on your marketing homepage?&lt;/p&gt;

&lt;p&gt;In addition to deep dives into the world of SQL queries, CSV dumps, and pretty charts and graphs generated from product usage data, I include data from sales and marketing in this channel. Understanding what leads are coming in and which are moving through your conversion funnel helps you build a better product. This raw data can be a great way to identify new market opportunities or ways to tighten up the funnel with a stronger product.&lt;/p&gt;

&lt;p&gt;Reviewing and analyzing engagement data should be part of your daily routine. As your product grows the amount of data you have about how your product is used can quickly get overwhelming. Stay on top of the data and build good processes for yourself to automate reporting on key metrics to make this channel more valuable.&lt;/p&gt;

&lt;h2 id=&quot;bringing-it-together&quot;&gt;Bringing it together&lt;/h2&gt;
&lt;p&gt;Learning to balance all of these inputs takes time, experience, and plenty of false starts. Your personality and your background will naturally attract you to spending more time on certain channels — if you love talking to people you might overemphasize conversational channels. If talking to people takes a lot of energy for you (that’s me) then you may find yourself leaning into data and spreadsheets and missing out on important conversations.&lt;/p&gt;

&lt;p&gt;Ensure that you are using the full array of input channels by building routines that force you to consider each channel, and evaluate the source of the items that make it onto your roadmap to hold yourself accountable to staying engaged with each channel.&lt;/p&gt;

&lt;p&gt;Finally, remember that tuning into all of these different input channels is a privilege that most people at your company won’t get to have — think about ways to surface the insights that you glean from spending so much time processing this feedback to the rest of your company. As product managers, we have an opportunity to help propagate a deep understanding of our products, how people feel about them, and how people use them throughout our whole company. Invest in that opportunity and you may be surprised at your returns.&lt;/p&gt;

</content>
 </entry>
 

</feed>
